Alpha
This commit is contained in:
@@ -12,3 +12,6 @@
|
||||
padding-left: 36px;
|
||||
border-bottom: 1px #CCC solid
|
||||
}
|
||||
div {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
@@ -665,7 +665,6 @@ const AppThemed = withStyles(appStyles)(class extends ResizeResponsiveComponent<
|
||||
size="large">
|
||||
<MenuButton />
|
||||
</IconButton>
|
||||
<div style={{ flex: "1 1 1px" }} />
|
||||
<div style={{ flex: "0 1 400px", minWidth: 100 }}>
|
||||
<PresetSelector />
|
||||
</div>
|
||||
|
||||
@@ -21,7 +21,7 @@ import React from 'react';
|
||||
import { PiPedalModel, PiPedalModelFactory } from './PiPedalModel';
|
||||
|
||||
|
||||
import {PedalboardItem, PedalboardSplitItem} from './Pedalboard';
|
||||
import { PedalboardItem, PedalboardSplitItem } from './Pedalboard';
|
||||
import PluginControlView from './PluginControlView';
|
||||
import SplitControlView from './SplitControlView';
|
||||
import Typography from '@mui/material/Typography';
|
||||
@@ -29,10 +29,10 @@ import IControlViewFactory from './IControlViewFactory';
|
||||
import ToobInputStageViewFactory from './ToobInputStageView';
|
||||
import ToobToneStackViewFactory from './ToobToneStackView';
|
||||
import ToobCabSimViewFactory from './ToobCabSimView';
|
||||
import ToobPowerStage2Factory from './ToobPowerStage2View';
|
||||
import ToobPowerStage2Factory from './ToobPowerStage2View';
|
||||
import ToobSpectrumAnalyzerViewFactory from './ToobSpectrumAnalyzerView';
|
||||
import ToobMLViewFactory from './ToobMLView';
|
||||
import {ToobTunerViewFactory,GxTunerViewFactory} from './GxTunerView';
|
||||
import { ToobTunerViewFactory, GxTunerViewFactory } from './GxTunerView';
|
||||
|
||||
|
||||
let pluginFactories: IControlViewFactory[] = [
|
||||
@@ -47,32 +47,32 @@ let pluginFactories: IControlViewFactory[] = [
|
||||
];
|
||||
|
||||
|
||||
export function GetControlView(pedalboardItem?: PedalboardItem| null): React.ReactNode
|
||||
{
|
||||
export function GetControlView(pedalboardItem?: PedalboardItem | null): React.ReactNode {
|
||||
let model: PiPedalModel = PiPedalModelFactory.getInstance();
|
||||
|
||||
if (!pedalboardItem) {
|
||||
return (<div/>);
|
||||
return (<div />);
|
||||
}
|
||||
if (pedalboardItem.isSplit())
|
||||
{
|
||||
if (pedalboardItem.isStart() || pedalboardItem.isEnd()) {
|
||||
return (
|
||||
<PluginControlView instanceId={pedalboardItem.instanceId} item={pedalboardItem} />
|
||||
);
|
||||
}
|
||||
if (pedalboardItem.isSplit()) {
|
||||
return (
|
||||
<SplitControlView item={pedalboardItem as PedalboardSplitItem} instanceId={pedalboardItem!.instanceId}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
for (let i = 0; i < pluginFactories.length; ++i)
|
||||
{
|
||||
for (let i = 0; i < pluginFactories.length; ++i) {
|
||||
let factory = pluginFactories[i];
|
||||
if (factory.uri === pedalboardItem.uri)
|
||||
{
|
||||
return factory.Create(model,pedalboardItem);
|
||||
if (factory.uri === pedalboardItem.uri) {
|
||||
return factory.Create(model, pedalboardItem);
|
||||
}
|
||||
}
|
||||
let uiPlugin = model.getUiPlugin(pedalboardItem.uri);
|
||||
if (!uiPlugin)
|
||||
{
|
||||
<div style={{paddingLeft: 40, paddingRight: 40}}>
|
||||
if (!uiPlugin) {
|
||||
<div style={{ paddingLeft: 40, paddingRight: 40 }}>
|
||||
<Typography color="error" variant="h6" >Missing plugin.</Typography>
|
||||
<Typography>The plugin '{pedalboardItem.pluginName}' ({pedalboardItem.uri}) is not currently installed.</Typography>
|
||||
</div>
|
||||
|
||||
@@ -36,8 +36,12 @@ const LONG_PRESS_TIME_MS = 250;
|
||||
|
||||
const styles = (theme: Theme) => createStyles({
|
||||
frame: {
|
||||
position: "relative",
|
||||
margin: "12px"
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
position: "relative"
|
||||
}
|
||||
});
|
||||
|
||||
@@ -479,7 +483,7 @@ const Draggable =
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div style={{ transform: "" }}
|
||||
<div className={this.props.classes.frame} style={{ transform: "" }}
|
||||
onPointerDown={this.onPointerDown}
|
||||
onPointerMove={this.onPointerMove}
|
||||
onPointerCancel={this.onPointerCancel}
|
||||
|
||||
@@ -29,14 +29,16 @@ import ArrowBackIcon from '@mui/icons-material/ArrowBack';
|
||||
|
||||
import DialogActions from '@mui/material/DialogActions';
|
||||
import DialogTitle from '@mui/material/DialogTitle';
|
||||
import DeleteIcon from '@mui/icons-material/Delete';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import OldDeleteIcon from './OldDeleteIcon';
|
||||
|
||||
import ResizeResponsiveComponent from './ResizeResponsiveComponent';
|
||||
import { PiPedalFileProperty } from './Lv2Plugin';
|
||||
import ButtonBase from '@mui/material/ButtonBase';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import DialogEx from './DialogEx';
|
||||
import UploadFileDialog from './UploadFileDialog';
|
||||
import OkCancelDialog from './OkCancelDialog';
|
||||
|
||||
export interface FilePropertyDialogProps {
|
||||
open: boolean,
|
||||
@@ -51,6 +53,8 @@ export interface FilePropertyDialogState {
|
||||
selectedFile: string;
|
||||
hasSelection: boolean;
|
||||
files: string[];
|
||||
openUploadFileDialog: boolean;
|
||||
openConfirmDeleteDialog: boolean;
|
||||
};
|
||||
|
||||
export default class FilePropertyDialog extends ResizeResponsiveComponent<FilePropertyDialogProps, FilePropertyDialogState> {
|
||||
@@ -66,7 +70,9 @@ export default class FilePropertyDialog extends ResizeResponsiveComponent<FilePr
|
||||
fullScreen: false,
|
||||
selectedFile: props.selectedFile,
|
||||
hasSelection: false,
|
||||
files: []
|
||||
files: [],
|
||||
openUploadFileDialog: false,
|
||||
openConfirmDeleteDialog: false
|
||||
};
|
||||
|
||||
this.requestFiles();
|
||||
@@ -74,32 +80,22 @@ export default class FilePropertyDialog extends ResizeResponsiveComponent<FilePr
|
||||
private mounted: boolean = false;
|
||||
private model: PiPedalModel;
|
||||
|
||||
private lastFileProperty: PiPedalFileProperty = new PiPedalFileProperty();
|
||||
|
||||
private requestFiles() {
|
||||
if (!this.props.open)
|
||||
{
|
||||
if (!this.props.open) {
|
||||
return;
|
||||
}
|
||||
if (this.props.fileProperty.directory === "")
|
||||
{
|
||||
if (this.props.fileProperty.directory === "") {
|
||||
return;
|
||||
}
|
||||
if (this.lastFileProperty === this.props.fileProperty)
|
||||
{
|
||||
return;
|
||||
}
|
||||
this.lastFileProperty = this.props.fileProperty;
|
||||
|
||||
this.model.requestFileList(this.props.fileProperty)
|
||||
.then((files) => {
|
||||
if (this.mounted)
|
||||
{
|
||||
this.setState({files: files,hasSelection: this.isFileInList(files,this.state.selectedFile)});
|
||||
}
|
||||
}).catch((error)=>{
|
||||
this.model.showAlert(error.toString())
|
||||
});
|
||||
.then((files) => {
|
||||
if (this.mounted) {
|
||||
this.setState({ files: files, hasSelection: this.isFileInList(files, this.state.selectedFile) });
|
||||
}
|
||||
}).catch((error) => {
|
||||
this.model.showAlert(error.toString())
|
||||
});
|
||||
}
|
||||
|
||||
onWindowSizeChanged(width: number, height: number): void {
|
||||
@@ -116,11 +112,17 @@ export default class FilePropertyDialog extends ResizeResponsiveComponent<FilePr
|
||||
this.mounted = false;
|
||||
}
|
||||
componentDidUpdate(prevProps: Readonly<FilePropertyDialogProps>, prevState: Readonly<FilePropertyDialogState>, snapshot?: any): void {
|
||||
super.componentDidUpdate?.(prevProps,prevState,snapshot);
|
||||
if (prevProps.fileProperty !== this.props.fileProperty)
|
||||
super.componentDidUpdate?.(prevProps, prevState, snapshot);
|
||||
if (prevProps.open !== this.props.open)
|
||||
{
|
||||
if (this.props.open)
|
||||
{
|
||||
this.requestFiles()
|
||||
}
|
||||
} else if (prevProps.fileProperty !== this.props.fileProperty) {
|
||||
this.requestFiles()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private fileNameOnly(path: string): string {
|
||||
@@ -139,7 +141,7 @@ export default class FilePropertyDialog extends ResizeResponsiveComponent<FilePr
|
||||
|
||||
}
|
||||
|
||||
private isFileInList(files: string[],file: string) {
|
||||
private isFileInList(files: string[], file: string) {
|
||||
|
||||
let hasSelection = false;
|
||||
for (var listFile of files) {
|
||||
@@ -152,7 +154,52 @@ export default class FilePropertyDialog extends ResizeResponsiveComponent<FilePr
|
||||
}
|
||||
|
||||
onSelect(selectedFile: string) {
|
||||
this.setState({ selectedFile: selectedFile, hasSelection: this.isFileInList(this.state.files,selectedFile) })
|
||||
this.setState({ selectedFile: selectedFile, hasSelection: this.isFileInList(this.state.files, selectedFile) })
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
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) {
|
||||
newSelection = this.state.files[position-1];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
this.model.deleteUserFile(this.state.selectedFile)
|
||||
.then(
|
||||
()=> {
|
||||
this.setState({selectedFile: newSelection,hasSelection: newSelection !== ""});
|
||||
this.requestFiles();
|
||||
}
|
||||
).catch(
|
||||
(e: any)=>
|
||||
{
|
||||
this.model.showAlert(e + "");
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
mapKey: number = 0;
|
||||
@@ -160,63 +207,90 @@ export default class FilePropertyDialog extends ResizeResponsiveComponent<FilePr
|
||||
this.mapKey = 0;
|
||||
return this.props.open &&
|
||||
(
|
||||
<DialogEx onClose={() => this.props.onCancel()} open={this.props.open} tag="FilePropertyDialog" fullWidth maxWidth="xl" style={{ height: "90%" }}
|
||||
PaperProps={{ style: { minHeight: "90%", maxHeight: "90%", overflowY: "visible" } }}
|
||||
>
|
||||
<DialogTitle >
|
||||
<div>
|
||||
<IconButton edge="start" color="inherit" onClick={()=> {this.props.onCancel();}} aria-label="back"
|
||||
>
|
||||
<ArrowBackIcon fontSize="small" style={{opacity: "0.6"}} />
|
||||
</IconButton>
|
||||
<Typography display="inline" >{this.props.fileProperty.name}</Typography>
|
||||
</div>
|
||||
</DialogTitle>
|
||||
<div style={{ flex: "0 0 auto", height: "1px", background: "rgba(0,0,0,0.2" }}> </div>
|
||||
<div style={{ flex: "1 1 auto", height: "300", display: "flex", flexFlow: "row nowrap", overflowX: "auto", overflowY: "visible" }}>
|
||||
<div style={{ flex: "1 1 100%", display: "flex", flexFlow: "column wrap", justifyContent: "start", alignItems: "flex-start",paddingLeft: 16,paddingTop:16 }}>
|
||||
{
|
||||
this.state.files.map(
|
||||
(value: string, index: number) => {
|
||||
let selected = value === this.state.selectedFile;
|
||||
let selectBg = selected ? "rgba(0,0,0,0.15)" : "rgba(0,0,0,0.0)";
|
||||
return (
|
||||
<ButtonBase key={ this.mapKey++ }
|
||||
style={{ width: "320px", flex: "0 0 48px", position: "relative"}}
|
||||
onClick={() => this.onSelect(value)}
|
||||
>
|
||||
<div style={{ position: "absolute", background: selectBg, width: "100%", height: "100%", borderRadius: 4 }} />
|
||||
<div style={{ display: "flex", flexFlow: "row nowrap", justifyContent: "start", alignItems: "center", width: "100%", height: "100%" }}>
|
||||
<AudioFileIcon style={{ flex: "0 0 auto", opacity: 0.7, marginRight: 8, marginLeft: 8 }} />
|
||||
<Typography noWrap style={{ flex: "1 1 auto", textAlign: "left" }}>{this.fileNameOnly(value)}</Typography>
|
||||
</div>
|
||||
</ButtonBase>
|
||||
);
|
||||
}
|
||||
)
|
||||
}
|
||||
<DialogEx onClose={() => this.props.onCancel()} open={this.props.open} tag="FilePropertyDialog" fullWidth maxWidth="xl" style={{ height: "90%" }}
|
||||
PaperProps={{ style: { minHeight: "90%", maxHeight: "90%", overflowY: "visible" } }}
|
||||
>
|
||||
<DialogTitle >
|
||||
<div>
|
||||
<IconButton edge="start" color="inherit" onClick={() => { this.props.onCancel(); }} aria-label="back"
|
||||
>
|
||||
<ArrowBackIcon fontSize="small" style={{ opacity: "0.6" }} />
|
||||
</IconButton>
|
||||
<Typography display="inline" >{this.props.fileProperty.name}</Typography>
|
||||
</div>
|
||||
</DialogTitle>
|
||||
<div style={{ flex: "0 0 auto", height: "1px", background: "rgba(0,0,0,0.2" }}> </div>
|
||||
<div style={{ flex: "1 1 auto", height: "300", display: "flex", flexFlow: "row nowrap", overflowX: "auto", overflowY: "visible" }}>
|
||||
<div style={{ flex: "1 1 100%", display: "flex", flexFlow: "column wrap", justifyContent: "start", alignItems: "flex-start", paddingLeft: 16, paddingTop: 16 }}>
|
||||
{
|
||||
this.state.files.map(
|
||||
(value: string, index: number) => {
|
||||
let selected = value === this.state.selectedFile;
|
||||
let selectBg = selected ? "rgba(0,0,0,0.15)" : "rgba(0,0,0,0.0)";
|
||||
return (
|
||||
<ButtonBase key={this.mapKey++}
|
||||
style={{ width: "320px", flex: "0 0 48px", position: "relative" }}
|
||||
onClick={() => this.onSelect(value)}
|
||||
>
|
||||
<div style={{ position: "absolute", background: selectBg, width: "100%", height: "100%", borderRadius: 4 }} />
|
||||
<div style={{ display: "flex", flexFlow: "row nowrap", justifyContent: "start", alignItems: "center", width: "100%", height: "100%" }}>
|
||||
<AudioFileIcon style={{ flex: "0 0 auto", opacity: 0.7, marginRight: 8, marginLeft: 8 }} />
|
||||
<Typography noWrap style={{ flex: "1 1 auto", textAlign: "left" }}>{this.fileNameOnly(value)}</Typography>
|
||||
</div>
|
||||
</ButtonBase>
|
||||
);
|
||||
}
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ flex: "0 0 auto", height: "1px", background: "rgba(0,0,0,0.2" }}> </div>
|
||||
<DialogActions style={{ justifyContent: "stretch" }}>
|
||||
<div style={{ display: "flex", width: "100%", flexFlow: "row nowrap" }}>
|
||||
<Button style={{ flex: "0 0 auto" }} startIcon={<FileUploadIcon />}>
|
||||
<input hidden accept="audio/x-wav" name="Upload Impuse File" type="file" multiple />
|
||||
Upload
|
||||
</Button>
|
||||
<IconButton style={{visibility: (this.state.hasSelection? "visible": "hidden")}} aria-label="delete selected file" component="label" color="primary" >
|
||||
<DeleteIcon fontSize='small' />
|
||||
</IconButton>
|
||||
<div style={{ flex: "1 1 auto" }}> </div>
|
||||
<Button onClick={() => {this.props.onCancel();}} aria-label="cancel">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button style={{ flex: "0 0 auto" }} onClick={() => {this.props.onOk(this.props.fileProperty,this.state.selectedFile);}} color="secondary" disabled={!this.state.hasSelection} aria-label="select">
|
||||
Select
|
||||
</Button>
|
||||
</div>
|
||||
</DialogActions>
|
||||
</DialogEx>
|
||||
);
|
||||
<div style={{ flex: "0 0 auto", height: "1px", background: "rgba(0,0,0,0.2" }}> </div>
|
||||
<DialogActions style={{ justifyContent: "stretch" }}>
|
||||
<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"
|
||||
onClick={()=> this.handleDelete()} >
|
||||
<OldDeleteIcon fontSize='small' />
|
||||
</IconButton>
|
||||
|
||||
<Button style={{ flex: "0 0 auto" }} aria-label="upload" variant="text" startIcon={<FileUploadIcon />}
|
||||
onClick={()=> { this.setState({openUploadFileDialog: true})}}
|
||||
>
|
||||
<div>Upload</div>
|
||||
</Button>
|
||||
<div style={{ flex: "1 1 auto" }}> </div>
|
||||
|
||||
<Button onClick={() => { this.props.onCancel(); }} aria-label="cancel">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button style={{ flex: "0 0 auto" }} onClick={() => { this.props.onOk(this.props.fileProperty, this.state.selectedFile); }} color="secondary" disabled={!this.state.hasSelection} aria-label="select">
|
||||
Select
|
||||
</Button>
|
||||
</div>
|
||||
</DialogActions>
|
||||
<UploadFileDialog
|
||||
open={this.state.openUploadFileDialog}
|
||||
onClose=
|
||||
{
|
||||
() => {
|
||||
this.setState({ openUploadFileDialog: false });
|
||||
}
|
||||
}
|
||||
uploadPage={ "uploadUserFile?directory="+ encodeURIComponent(this.props.fileProperty.directory)}
|
||||
onUploaded={()=> { this.requestFiles();}}
|
||||
fileProperty={this.props.fileProperty}
|
||||
|
||||
|
||||
/>
|
||||
<OkCancelDialog open={this.state.openConfirmDeleteDialog}
|
||||
text="Are you sure you want to delete the selected file?"
|
||||
okButtonText="Delete"
|
||||
onOk={()=>this.handleConfirmDelete()}
|
||||
onClose={()=>{
|
||||
this.setState({openConfirmDeleteDialog: false});
|
||||
}}
|
||||
|
||||
/>
|
||||
</DialogEx>
|
||||
);
|
||||
}
|
||||
}
|
||||
+191
-80
@@ -17,7 +17,7 @@
|
||||
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
import React, { ReactNode, SyntheticEvent, CSSProperties } from 'react';
|
||||
import React, { ReactNode, SyntheticEvent, CSSProperties, Fragment, ReactElement } from 'react';
|
||||
|
||||
import { PiPedalModel, PiPedalModelFactory, FavoritesList } from './PiPedalModel';
|
||||
import { UiPlugin, PluginType } from './Lv2Plugin';
|
||||
@@ -42,7 +42,6 @@ import SearchFilter from './SearchFilter';
|
||||
import { FixedSizeGrid } from 'react-window';
|
||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||
import StarBorderIcon from '@mui/icons-material/StarBorder';
|
||||
import Slide, { SlideProps } from '@mui/material/Slide';
|
||||
import { createStyles, Theme } from '@mui/material/styles';
|
||||
import { WithStyles, withStyles } from '@mui/styles';
|
||||
import FilterListIcon from '@mui/icons-material/FilterList';
|
||||
@@ -102,10 +101,10 @@ const pluginGridStyles = (theme: Theme) => createStyles({
|
||||
height: "100%'"
|
||||
},
|
||||
content: {
|
||||
marginTop: "8px",
|
||||
marginBottom: "8px",
|
||||
marginLeft: "12px",
|
||||
marginRight: "12px",
|
||||
marginTop: "4px",
|
||||
marginBottom: "4px",
|
||||
marginLeft: "8px",
|
||||
marginRight: "8px",
|
||||
width: "100%",
|
||||
overflow: "hidden",
|
||||
textAlign: "left",
|
||||
@@ -118,7 +117,7 @@ const pluginGridStyles = (theme: Theme) => createStyles({
|
||||
},
|
||||
content2: {
|
||||
display: "flex", flexDirection: "column", flex: "1 1 auto", width: "100%",
|
||||
paddingLeft: 16, whiteSpace: "nowrap"
|
||||
paddingLeft: 12, whiteSpace: "nowrap", textOverflow: "ellipsis"
|
||||
|
||||
},
|
||||
favoriteDecoration: {
|
||||
@@ -134,7 +133,8 @@ const pluginGridStyles = (theme: Theme) => createStyles({
|
||||
opacity: "0.6"
|
||||
},
|
||||
iconBorder: {
|
||||
flex: "0 0 auto"
|
||||
flex: "0 0 auto",
|
||||
paddingTop: "4px"
|
||||
},
|
||||
label: {
|
||||
width: "100%"
|
||||
@@ -173,14 +173,11 @@ type PluginGridState = {
|
||||
grid_cell_columns: number,
|
||||
minimumItemWidth: number,
|
||||
favoritesList: FavoritesList
|
||||
gridItems: UiPlugin[];
|
||||
|
||||
|
||||
}
|
||||
|
||||
const Transition = React.forwardRef(function Transition(
|
||||
props: SlideProps, ref: React.Ref<unknown>
|
||||
) {
|
||||
return (<Slide direction="up" ref={ref} {...props} />);
|
||||
});
|
||||
|
||||
export const LoadPluginDialog =
|
||||
withStyles(pluginGridStyles, { withTheme: true })(
|
||||
@@ -210,7 +207,9 @@ export const LoadPluginDialog =
|
||||
grid_cell_width: this.getCellWidth(window.innerWidth),
|
||||
grid_cell_columns: this.getCellColumns(window.innerWidth),
|
||||
minimumItemWidth: props.minimumItemWidth ? props.minimumItemWidth : 220,
|
||||
favoritesList: this.model.favorites.get()
|
||||
favoritesList: this.model.favorites.get(),
|
||||
gridItems: this.getFilteredPlugins("", filterType_, this.model.favorites.get())
|
||||
|
||||
};
|
||||
|
||||
this.updateWindowSize = this.updateWindowSize.bind(this);
|
||||
@@ -219,6 +218,8 @@ export const LoadPluginDialog =
|
||||
this.handleSearchStringReady = this.handleSearchStringReady.bind(this);
|
||||
this.handleKeyPress = this.handleKeyPress.bind(this);
|
||||
this.handleFavoritesChanged = this.handleFavoritesChanged.bind(this);
|
||||
|
||||
this.requestScrollTo();
|
||||
}
|
||||
|
||||
nominal_column_width: number = 250;
|
||||
@@ -252,9 +253,9 @@ export const LoadPluginDialog =
|
||||
|
||||
let newValue = searchInput.value + e.key;
|
||||
searchInput.value = newValue; // add the key. to the value.
|
||||
this.handleSearchStringChanged(newValue);
|
||||
searchInput.focus();
|
||||
this.setState({
|
||||
search_string: newValue,
|
||||
search_collapsed: false
|
||||
})
|
||||
|
||||
@@ -275,9 +276,12 @@ export const LoadPluginDialog =
|
||||
}
|
||||
|
||||
handleFavoritesChanged() {
|
||||
let favorites = this.model.favorites.get();
|
||||
this.requestScrollTo();
|
||||
this.setState(
|
||||
{
|
||||
favoritesList: this.model.favorites.get()
|
||||
favoritesList: favorites,
|
||||
gridItems: this.getFilteredPlugins(null, null, favorites)
|
||||
});
|
||||
}
|
||||
componentDidMount() {
|
||||
@@ -285,7 +289,12 @@ export const LoadPluginDialog =
|
||||
this.updateWindowSize();
|
||||
window.addEventListener('resize', this.updateWindowSize);
|
||||
this.model.favorites.addOnChangedHandler(this.handleFavoritesChanged);
|
||||
this.setState({ favoritesList: this.model.favorites.get() });
|
||||
let favorites = this.model.favorites.get();
|
||||
this.setState({
|
||||
favoritesList: favorites,
|
||||
gridItems: this.getFilteredPlugins(null, null, favorites)
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
componentWillUnmount() {
|
||||
@@ -297,7 +306,15 @@ export const LoadPluginDialog =
|
||||
componentDidUpdate(oldProps: PluginGridProps) {
|
||||
if (oldProps.open !== this.props.open) {
|
||||
if (this.props.open) {
|
||||
this.setState({ search_string: "", search_collapsed: true });
|
||||
// reset state now.
|
||||
if (this.state.search_string !== "" || !this.state.search_collapsed) {
|
||||
this.setState({
|
||||
search_string: "",
|
||||
search_collapsed: true,
|
||||
gridItems: this.getFilteredPlugins("", null, null)
|
||||
});
|
||||
}
|
||||
this.requestScrollTo();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -308,17 +325,26 @@ export const LoadPluginDialog =
|
||||
}
|
||||
|
||||
onFilterChange(e: any) {
|
||||
let value = e.target.value as PluginType;
|
||||
let filterValue = e.target.value as PluginType;
|
||||
|
||||
window.localStorage.setItem(FILTER_STORAGE_KEY, value as string);
|
||||
window.localStorage.setItem(FILTER_STORAGE_KEY, filterValue as string);
|
||||
|
||||
this.setState({ filterType: value });
|
||||
this.requestScrollTo();
|
||||
this.setState({
|
||||
filterType: filterValue,
|
||||
gridItems: this.getFilteredPlugins(null, filterValue, null)
|
||||
});
|
||||
}
|
||||
onClearFilter(): void {
|
||||
let value = PluginType.Plugin;
|
||||
window.localStorage.setItem(FILTER_STORAGE_KEY, value as string);
|
||||
|
||||
this.setState({ filterType: value });
|
||||
if (this.state.filterType !== value) {
|
||||
this.requestScrollTo();
|
||||
this.setState({
|
||||
filterType: value,
|
||||
gridItems: this.getFilteredPlugins(null, value, null)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
selectItem(item: number): void {
|
||||
@@ -389,11 +415,9 @@ export const LoadPluginDialog =
|
||||
// let selectedUri = this.state.selected_uri;
|
||||
}
|
||||
|
||||
vst3_indicator(uiPlugin? : UiPlugin)
|
||||
{
|
||||
if (uiPlugin?.is_vst3)
|
||||
{
|
||||
return (<img alt="vst" src="/img/vst.svg" style={{marginLeft: 8,height: 22, opacity: 0.6,position: "relative",top: -1}} />)
|
||||
vst3_indicator(uiPlugin?: UiPlugin) {
|
||||
if (uiPlugin?.is_vst3) {
|
||||
return (<img alt="vst" src="/img/vst.svg" style={{ marginLeft: 8, height: 22, opacity: 0.6, position: "relative", top: -1 }} />)
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -404,14 +428,36 @@ export const LoadPluginDialog =
|
||||
}
|
||||
return "";
|
||||
}
|
||||
info_string(uiPlugin?: UiPlugin): string {
|
||||
if (uiPlugin === undefined) return "";
|
||||
let result = uiPlugin.name;
|
||||
if (uiPlugin.author_name !== "") {
|
||||
result += ", " + uiPlugin.author_name;
|
||||
info_string(uiPlugin?: UiPlugin): ReactElement {
|
||||
if (!uiPlugin) {
|
||||
return (<Fragment />);
|
||||
} else {
|
||||
let stereoIndicator = this.stereo_indicator(uiPlugin)
|
||||
if (uiPlugin.author_name !== "") {
|
||||
if (uiPlugin.author_homepage !== "") {
|
||||
return (<Fragment>
|
||||
<div style={{flex: "0 1 auto"}}>
|
||||
<Typography variant='body2' color="textSecondary" noWrap>{uiPlugin.name + ",\u00A0"}</Typography>
|
||||
</div>
|
||||
<div style={{flex: "0 1 auto"}}>
|
||||
<a href={uiPlugin.author_homepage} target="_blank" >
|
||||
<Typography variant='body2' color="textSecondary" noWrap>{uiPlugin.author_name}</Typography>
|
||||
</a>
|
||||
</div>
|
||||
</Fragment>);
|
||||
|
||||
} else {
|
||||
return (<Fragment>
|
||||
<Typography variant='body2' color="textSecondary" noWrap>{uiPlugin.name + ", " + uiPlugin.author_name}</Typography>
|
||||
</Fragment>);
|
||||
}
|
||||
}
|
||||
return (
|
||||
<Fragment>
|
||||
<Typography variant='body2' color="textSecondary" noWrap>{uiPlugin?.name ?? ""}</Typography>
|
||||
</Fragment>);
|
||||
|
||||
}
|
||||
result += this.stereo_indicator(uiPlugin);
|
||||
return result;
|
||||
}
|
||||
createFilterChildren(result: ReactNode[], classNode: PluginClass, level: number): void {
|
||||
for (let i = 0; i < classNode.children.length; ++i) {
|
||||
@@ -432,29 +478,35 @@ export const LoadPluginDialog =
|
||||
return result;
|
||||
|
||||
}
|
||||
getFilteredPlugins(): UiPlugin[] {
|
||||
getFilteredPlugins(searchString: string | null, filterType: PluginType | null, favoritesList: FavoritesList | null): UiPlugin[] {
|
||||
if (searchString === null) {
|
||||
searchString = this.state.search_string;
|
||||
}
|
||||
if (filterType === null) {
|
||||
filterType = this.state.filterType;
|
||||
}
|
||||
if (favoritesList === null) {
|
||||
favoritesList = this.state.favoritesList;
|
||||
}
|
||||
let plugins = this.model.ui_plugins.get();
|
||||
let searchString = this.state.search_string;
|
||||
|
||||
let results: { score: number; plugin: UiPlugin }[] = [];
|
||||
let searchFilter = new SearchFilter(searchString);
|
||||
let filterType = this.state.filterType;
|
||||
let rootClass = this.model.plugin_classes.get();
|
||||
|
||||
|
||||
for (let i = 0; i < plugins.length; ++i) {
|
||||
let plugin = plugins[i];
|
||||
if (filterType === PluginType.Plugin || rootClass.is_type_of(filterType, plugin.plugin_type)) {
|
||||
let score:number = 0;
|
||||
if (plugin.is_vst3)
|
||||
{
|
||||
score = searchFilter.score(plugin.name, plugin.plugin_display_type, plugin.author_name,"vst3");
|
||||
let score: number = 0;
|
||||
if (plugin.is_vst3) {
|
||||
score = searchFilter.score(plugin.name, plugin.plugin_display_type, plugin.author_name, "vst3");
|
||||
} else {
|
||||
score = searchFilter.score(plugin.name, plugin.plugin_display_type, plugin.author_name);
|
||||
}
|
||||
|
||||
if (score !== 0) {
|
||||
if (this.state.favoritesList[plugin.uri]) {
|
||||
if (favoritesList[plugin.uri]) {
|
||||
score += 32768;
|
||||
}
|
||||
results.push({ score: score, plugin: plugin });
|
||||
@@ -476,9 +528,38 @@ export const LoadPluginDialog =
|
||||
changedSearchString?: string = undefined;
|
||||
hSearchTimeout?: NodeJS.Timeout;
|
||||
|
||||
private scrollToRequested = false;
|
||||
requestScrollTo() {
|
||||
this.scrollToRequested = true;
|
||||
}
|
||||
|
||||
handleScrollToCallback(element: FixedSizeGrid): void {
|
||||
if (element) {
|
||||
if (this.scrollToRequested) {
|
||||
this.scrollToRequested = false;
|
||||
let position = -1;
|
||||
let gridItems = this.state.gridItems;
|
||||
for (let i = 0; i < gridItems.length; ++i) {
|
||||
if (this.state.selected_uri === gridItems[i].uri) {
|
||||
position = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (position != -1) {
|
||||
element.scrollToItem({ rowIndex: Math.floor(position / this.state.grid_cell_columns) });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
handleSearchStringReady() {
|
||||
if (this.changedSearchString !== undefined) {
|
||||
this.setState({ search_string: this.changedSearchString });
|
||||
this.requestScrollTo();
|
||||
this.setState({
|
||||
search_string: this.changedSearchString,
|
||||
gridItems: this.getFilteredPlugins(this.changedSearchString, null, null)
|
||||
});
|
||||
this.changedSearchString = undefined;
|
||||
}
|
||||
this.hSearchTimeout = undefined;
|
||||
@@ -498,14 +579,15 @@ export const LoadPluginDialog =
|
||||
}
|
||||
renderItem(row: number, column: number): React.ReactNode {
|
||||
let item: number = (row) * this.gridColumnCount + (column);
|
||||
|
||||
if (item >= this.gridItems.length) {
|
||||
let gridItems = this.state.gridItems;
|
||||
if (item >= gridItems.length) {
|
||||
return (<div />);
|
||||
}
|
||||
let value = this.gridItems[item];
|
||||
let value = gridItems[item];
|
||||
|
||||
let classes = this.props.classes;
|
||||
let isFavorite: boolean = this.state.favoritesList[value.uri] ?? false;
|
||||
|
||||
return (
|
||||
<div key={value.uri}
|
||||
onDoubleClick={(e) => { this.onDoubleClick(e, value.uri) }}
|
||||
@@ -520,16 +602,19 @@ export const LoadPluginDialog =
|
||||
<PluginIcon pluginType={value.plugin_type} pluginUri={value.uri} size={24} />
|
||||
</div>
|
||||
<div className={classes.content2}>
|
||||
<div className={classes.label} >
|
||||
<Typography color="textPrimary" noWrap sx={{ display: "inline" }} >
|
||||
{value.name}
|
||||
{
|
||||
isFavorite && (
|
||||
<StarBorderIcon sx={{ color: "#C80", fontSize: 16 }} />
|
||||
<div className={classes.label} style={{ display: "flex", flexFlow: "row nowrap", alignItems: "center" }} >
|
||||
|
||||
)
|
||||
}
|
||||
<Typography color="textPrimary" noWrap sx={{ display: "block", flex: "0 1 auto", }} >
|
||||
{value.name}
|
||||
</Typography>
|
||||
{
|
||||
isFavorite && (
|
||||
<div style={{ flex: "0 0 auto" }}>
|
||||
<StarBorderIcon sx={{ color: "#C80", fontSize: 16, marginRight: "2px" }} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
</div>
|
||||
<Typography color="textSecondary" noWrap>
|
||||
{value.plugin_display_type} {this.stereo_indicator(value)}
|
||||
@@ -549,7 +634,6 @@ export const LoadPluginDialog =
|
||||
if (!pluginUri) return;
|
||||
this.model.setFavorite(pluginUri, isFavorite);
|
||||
}
|
||||
gridItems: UiPlugin[] = [];
|
||||
gridColumnCount: number = 1;
|
||||
|
||||
render() {
|
||||
@@ -565,7 +649,6 @@ export const LoadPluginDialog =
|
||||
if (this.state.client_width < 500) {
|
||||
showSearchIcon = this.state.search_collapsed
|
||||
}
|
||||
this.gridItems = this.getFilteredPlugins();
|
||||
let gridColumnCount = this.state.grid_cell_columns;
|
||||
this.gridColumnCount = gridColumnCount;
|
||||
let isFavorite = this.state.favoritesList[this.state.selected_uri ?? ""];
|
||||
@@ -574,7 +657,7 @@ export const LoadPluginDialog =
|
||||
<Dialog
|
||||
onKeyPress={(e) => { this.handleKeyPress(e); }}
|
||||
fullScreen={true}
|
||||
TransitionComponent={Transition}
|
||||
TransitionComponent={undefined}
|
||||
maxWidth={false}
|
||||
open={this.props.open}
|
||||
scroll="body"
|
||||
@@ -603,13 +686,34 @@ export const LoadPluginDialog =
|
||||
this.handleSearchStringChanged(text);
|
||||
}}
|
||||
onClearFilterClick={() => {
|
||||
this.setState({ search_collapsed: true, search_string: "" });
|
||||
if (this.state.search_string !== "") {
|
||||
this.requestScrollTo();
|
||||
this.setState({
|
||||
search_collapsed: true,
|
||||
search_string: "",
|
||||
gridItems: this.getFilteredPlugins("", null, null)
|
||||
});
|
||||
|
||||
} else {
|
||||
this.setState({ search_collapsed: true });
|
||||
}
|
||||
}}
|
||||
onClick={() => {
|
||||
if (this.state.search_collapsed) {
|
||||
this.setState({ search_collapsed: false });
|
||||
} else {
|
||||
this.setState({ search_collapsed: true, search_string: "" });
|
||||
|
||||
if (this.state.search_string !== "") {
|
||||
this.requestScrollTo();
|
||||
this.setState({
|
||||
search_collapsed: true,
|
||||
search_string: "",
|
||||
gridItems: this.getFilteredPlugins("", null, null)
|
||||
});
|
||||
|
||||
} else {
|
||||
this.setState({ search_collapsed: true });
|
||||
}
|
||||
}
|
||||
|
||||
}}
|
||||
@@ -624,13 +728,13 @@ export const LoadPluginDialog =
|
||||
)}
|
||||
</IconButton>
|
||||
</div>
|
||||
<div style={{ flex: "0 0 160px",marginRight: 24 }} >
|
||||
<div style={{ flex: "0 0 160px", marginRight: 24 }} >
|
||||
|
||||
<TextField select variant="standard"
|
||||
defaultValue={this.state.filterType}
|
||||
key={this.state.filterType}
|
||||
onChange={(e) => { this.onFilterChange(e); }}
|
||||
sx={{ minWidth: 160}}
|
||||
sx={{ minWidth: 160 }}
|
||||
>
|
||||
{this.createFilterOptions()}
|
||||
</TextField>
|
||||
@@ -643,22 +747,33 @@ export const LoadPluginDialog =
|
||||
flex: "1 1 100px",
|
||||
}} >
|
||||
<AutoSizer>
|
||||
{(arg: { height: number, width: number }) => {
|
||||
{(arg) => {
|
||||
if ((!arg.width) || (!arg.height)) {
|
||||
return (<div></div>);
|
||||
}
|
||||
let width = arg.width ?? 1;
|
||||
let height = arg.height ?? 1;
|
||||
let gridItems = this.state.gridItems;
|
||||
|
||||
let scrollRef = (grid: FixedSizeGrid) => { this.handleScrollToCallback(grid); }
|
||||
|
||||
|
||||
return (
|
||||
<FixedSizeGrid
|
||||
width={arg.width}
|
||||
ref={scrollRef}
|
||||
width={width}
|
||||
columnCount={this.gridColumnCount}
|
||||
columnWidth={(arg.width - 40) / this.gridColumnCount}
|
||||
height={arg.height}
|
||||
columnWidth={(width - 40) / this.gridColumnCount}
|
||||
height={height}
|
||||
rowHeight={64}
|
||||
overscanRowCount={10}
|
||||
rowCount={Math.ceil(this.gridItems.length / this.gridColumnCount)}
|
||||
rowCount={Math.ceil(gridItems.length / this.gridColumnCount)}
|
||||
itemKey={(args: { columnIndex: number, data: any, rowIndex: number }) => {
|
||||
let index = args.columnIndex + this.gridColumnCount * args.rowIndex;
|
||||
if (index >= this.gridItems.length) {
|
||||
if (index >= gridItems.length) {
|
||||
return "blank-" + args.columnIndex + "-" + args.rowIndex;
|
||||
}
|
||||
let plugin = this.gridItems[index];
|
||||
let plugin = gridItems[index];
|
||||
return plugin.uri + "-" + args.rowIndex + "-" + args.columnIndex;
|
||||
|
||||
}}
|
||||
@@ -680,11 +795,9 @@ export const LoadPluginDialog =
|
||||
{(this.state.client_width >= NARROW_DISPLAY_THRESHOLD) ? (
|
||||
<DialogActions style={{ flex: "0 0 auto" }} >
|
||||
<div className={classes.bottom}>
|
||||
<div style={{ display: "flex", justifyContent: "start", alignItems: "center", flex: "1 1 auto", height: "100%", overflow: "hidden" }} >
|
||||
<div style={{ display: "flex", textOverflow: "ellipsis", justifyContent: "start", alignItems: "center", flex: "1 1 auto", height: "100%", overflow: "hidden" }} >
|
||||
<PluginInfoDialog plugin_uri={this.state.selected_uri ?? ""} />
|
||||
<Typography display='block' variant='body2' color="textPrimary" noWrap >
|
||||
{this.info_string(selectedPlugin)}
|
||||
</Typography>
|
||||
{this.info_string(selectedPlugin)}
|
||||
{this.vst3_indicator(selectedPlugin)}
|
||||
<div style={{
|
||||
color: isFavorite ? "#F80" : "#CCC", display: (this.state.selected_uri !== "" ? "block" : "none"),
|
||||
@@ -708,12 +821,10 @@ export const LoadPluginDialog =
|
||||
<div style={{ display: "flex", justifyContent: "start", alignItems: "center", flex: "1 1 auto", overflow: "hidden" }} >
|
||||
<PluginInfoDialog plugin_uri={this.state.selected_uri ?? ""} />
|
||||
<div style={{ width: 1, height: 48 }} />
|
||||
<Typography display='block' variant='body2' color="textPrimary" noWrap >
|
||||
{this.info_string(selectedPlugin)}
|
||||
</Typography>
|
||||
{this.info_string(selectedPlugin)}
|
||||
<div style={{
|
||||
color: isFavorite ? "#F80" : "#CCC", display: (this.state.selected_uri ? "block" : "block"),
|
||||
position: "relative", top: -5
|
||||
position: "relative"
|
||||
}}>
|
||||
<IconButton color="inherit" aria-label="Set as favorite"
|
||||
onClick={() => { this.setFavorite(this.state.selected_uri, !isFavorite); }}
|
||||
@@ -722,11 +833,11 @@ export const LoadPluginDialog =
|
||||
</IconButton>
|
||||
</div>
|
||||
</div>
|
||||
<div className={classes.bottom}>
|
||||
<div className={classes.bottom} style={{height: 64}}>
|
||||
<div style={{ flex: "1 1 1px" }} />
|
||||
<div style={{ position: "relative", flex: "0 1 auto", display: "flex", alignItems: "center" }}>
|
||||
<Button onClick={this.handleCancel} style={{ width: 120, height: 48 }} >Cancel</Button>
|
||||
<Button onClick={this.handleOk} color="secondary" disabled={selectedPlugin === null} style={{ width: 120, height: 48 }} >SELECT</Button>
|
||||
<Button onClick={this.handleCancel} style={{ height: 48 }} >Cancel</Button>
|
||||
<Button onClick={this.handleOk} color="secondary" disabled={selectedPlugin === null} style={{ height: 48 }} >SELECT</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DialogActions>
|
||||
|
||||
+72
-22
@@ -167,6 +167,37 @@ export class PiPedalFileProperty {
|
||||
return result;
|
||||
}
|
||||
|
||||
private static getFileExtension(name: string): string {
|
||||
let pos = name.lastIndexOf('.');
|
||||
let filenamePos = name.lastIndexOf('/') +1;
|
||||
filenamePos = Math.max(name.lastIndexOf('\\')+1);
|
||||
filenamePos = Math.max(name.lastIndexOf(':')+1);
|
||||
if (pos < filenamePos)
|
||||
{
|
||||
return "";
|
||||
}
|
||||
|
||||
if (pos !== -1) {
|
||||
return name.substring(pos);
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
wantsFile(filename: string) : boolean {
|
||||
if (this.fileTypes.length === 0) {
|
||||
return true;
|
||||
}
|
||||
let extension = PiPedalFileProperty.getFileExtension(filename);
|
||||
for (let fileType of this.fileTypes)
|
||||
{
|
||||
if (fileType.fileExtension === extension )
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
name: string = "";
|
||||
fileTypes: PiPedalFileType[] = [];
|
||||
patchProperty: string = "";
|
||||
@@ -290,7 +321,7 @@ export enum PluginType {
|
||||
export enum ControlType {
|
||||
Dial,
|
||||
OnOffSwitch,
|
||||
Toggle,
|
||||
ABSwitch,
|
||||
Select
|
||||
}
|
||||
|
||||
@@ -300,6 +331,7 @@ export class UiControl implements Deserializable<UiControl> {
|
||||
this.symbol = input.symbol;
|
||||
this.name = input.name;
|
||||
this.index = input.index;
|
||||
this.is_input = input.is_input;
|
||||
this.min_value = input.min_value;
|
||||
this.max_value = input.max_value;
|
||||
this.default_value = input.default_value;
|
||||
@@ -320,33 +352,55 @@ export class UiControl implements Deserializable<UiControl> {
|
||||
this.is_program_controller = input.is_program_controller? true: false;
|
||||
this.custom_units = input.custom_units ?? "";
|
||||
|
||||
this.controlType = ControlType.Dial;
|
||||
|
||||
if (this.is_bypass)
|
||||
{
|
||||
this.not_on_gui = true;
|
||||
}
|
||||
|
||||
this.controlType = ControlType.Dial;
|
||||
|
||||
if (this.enumeration_property && this.scale_points.length === 2)
|
||||
{
|
||||
this.controlType = ControlType.Toggle;
|
||||
} else {
|
||||
if (this.min_value === 0 && this.max_value === 1)
|
||||
{
|
||||
if (this.toggled_property || this.integer_property || this.range_steps === 2)
|
||||
{
|
||||
this.controlType = ControlType.OnOffSwitch;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (this.controlType === ControlType.Dial && this.enumeration_property)
|
||||
if (this.isValidEnumeration())
|
||||
{
|
||||
this.controlType = ControlType.Select;
|
||||
if (this.scale_points.length === 2)
|
||||
{
|
||||
this.controlType = ControlType.ABSwitch;
|
||||
}
|
||||
} else {
|
||||
if (this.toggled_property || (this.integer_property && this.min_value === 0 && this.max_value === 1))
|
||||
{
|
||||
this.controlType = ControlType.OnOffSwitch;
|
||||
}
|
||||
}
|
||||
return this;
|
||||
|
||||
}
|
||||
private hasScalePoint(value: number): boolean {
|
||||
for (let scale_point of this.scale_points)
|
||||
{
|
||||
if (scale_point.value === value) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
private isValidEnumeration() : boolean {
|
||||
if (this.enumeration_property) return true;
|
||||
if (this.toggled_property && this.min_value === 0 && this.max_value === 1)
|
||||
{
|
||||
if (this.hasScalePoint(this.min_value) && this.hasScalePoint(this.max_value))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (this.integer_property && this.min_value === 0 && this.max_value === 1)
|
||||
{
|
||||
if (this.hasScalePoint(this.min_value) && this.hasScalePoint(this.max_value))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
controlType: ControlType = ControlType.Dial; // non-serializable.
|
||||
|
||||
@@ -361,6 +415,7 @@ export class UiControl implements Deserializable<UiControl> {
|
||||
symbol: string = "";
|
||||
name: string = "";
|
||||
index: number = -1;
|
||||
is_input: boolean = true;
|
||||
min_value: number = 0;
|
||||
max_value: number = 1;
|
||||
default_value:number = 0.5;
|
||||
@@ -406,7 +461,7 @@ export class UiControl implements Deserializable<UiControl> {
|
||||
}
|
||||
|
||||
isAbToggle(): boolean {
|
||||
return this.controlType === ControlType.Toggle;
|
||||
return this.controlType === ControlType.ABSwitch;
|
||||
}
|
||||
isSelect() : boolean {
|
||||
return this.controlType === ControlType.Select;
|
||||
@@ -426,9 +481,6 @@ export class UiControl implements Deserializable<UiControl> {
|
||||
if (range > 1) range = 1;
|
||||
if (range < 0) range = 0;
|
||||
|
||||
if (this.range_steps !== 0) {
|
||||
range = Math.round(range*this.range_steps)/this.range_steps;
|
||||
}
|
||||
|
||||
return range;
|
||||
}
|
||||
@@ -438,9 +490,7 @@ export class UiControl implements Deserializable<UiControl> {
|
||||
if (range > 1) range = 1;
|
||||
|
||||
if (this.toggled_property) return range === 0? 0: 1;
|
||||
if (this.range_steps !== 0) {
|
||||
range = Math.round(range * this.range_steps) / this.range_steps;
|
||||
}
|
||||
|
||||
let value = range * (this.max_value - this.min_value) + this.min_value;
|
||||
if (this.integer_property || this.enumeration_property) {
|
||||
value = Math.round(value);
|
||||
|
||||
+90
-63
@@ -45,6 +45,7 @@ import PluginInfoDialog from './PluginInfoDialog';
|
||||
import { GetControlView } from './ControlViewFactory';
|
||||
import MidiBindingsDialog from './MidiBindingsDialog';
|
||||
import PluginPresetSelector from './PluginPresetSelector';
|
||||
import SaveIconOutline from '@mui/icons-material/Save';
|
||||
|
||||
|
||||
const SPLIT_CONTROLBAR_THRESHHOLD = 650;
|
||||
@@ -75,7 +76,7 @@ const styles = ({ palette }: Theme) => createStyles({
|
||||
flex: "0 0 auto", width: "100%", height: 48
|
||||
},
|
||||
splitControlBar: {
|
||||
flex: "0 0 48px", width: "100%", paddingLeft: 24, paddingRight: 16, paddingBottom: 16
|
||||
flex: "0 0 64px", width: "100%", paddingLeft: 24, paddingRight: 16, paddingBottom: 16
|
||||
},
|
||||
controlContent: {
|
||||
flex: "1 1 auto", width: "100%", overflowY: "auto", minHeight: 240
|
||||
@@ -83,8 +84,8 @@ const styles = ({ palette }: Theme) => createStyles({
|
||||
controlContentSmall: {
|
||||
flex: "0 0 162px", width: "100%", height: 162, overflowY: "hidden",
|
||||
},
|
||||
title: { fontSize: "1.1em", fontWeight: 700, marginRight: 8 },
|
||||
author: { fontWeight: 500, marginRight: 8 }
|
||||
title: { fontSize: "1.1em", fontWeight: 700, marginRight: 8, textOverflow: "ellipsis", whiteSpace: "nowrap", opacity: 0.75 },
|
||||
author: { fontWeight: 500, marginRight: 8, textOverflow: "ellipsis", whiteSpace: "nowrap",opacity: 0.75 }
|
||||
});
|
||||
|
||||
|
||||
@@ -103,7 +104,7 @@ interface MainState {
|
||||
horizontalScrollLayout: boolean;
|
||||
showMidiBindingsDialog: boolean;
|
||||
screenHeight: number;
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -128,7 +129,7 @@ export const MainPage =
|
||||
horizontalScrollLayout: this.windowSize.height < HORIZONTAL_CONTROL_SCROLL_HEIGHT_BREAK,
|
||||
showMidiBindingsDialog: false,
|
||||
screenHeight: this.windowSize.height
|
||||
|
||||
|
||||
};
|
||||
this.onSelectionChanged = this.onSelectionChanged.bind(this);
|
||||
this.onPedalDoubleClick = this.onPedalDoubleClick.bind(this);
|
||||
@@ -193,16 +194,17 @@ export const MainPage =
|
||||
}
|
||||
onPedalboardChanged(value: Pedalboard) {
|
||||
let selectedItem = -1;
|
||||
if (value.hasItem(this.state.selectedPedal))
|
||||
{
|
||||
if (this.state.selectedPedal == Pedalboard.START_CONTROL || this.state.selectedPedal == Pedalboard.END_CONTROL) {
|
||||
selectedItem = this.state.selectedPedal;
|
||||
} else if (value.hasItem(this.state.selectedPedal)) {
|
||||
selectedItem = this.state.selectedPedal;
|
||||
} else {
|
||||
selectedItem = value.getFirstSelectableItem();
|
||||
}
|
||||
this.setState({
|
||||
this.setState({
|
||||
pedalboard: value,
|
||||
selectedPedal: selectedItem
|
||||
});
|
||||
});
|
||||
}
|
||||
onDeletePedal(instanceId: number): void {
|
||||
let result = this.model.deletePedalboardPedal(instanceId);
|
||||
@@ -274,6 +276,15 @@ export const MainPage =
|
||||
|
||||
let pedalboard = this.model.pedalboard.get();
|
||||
if (!pedalboard) return null;
|
||||
|
||||
if (selectedId === Pedalboard.START_CONTROL) // synthetic input volume item.
|
||||
{
|
||||
return pedalboard.makeStartItem();
|
||||
} else if (selectedId === Pedalboard.END_CONTROL) // synthetic output volume.
|
||||
{
|
||||
return pedalboard.makeEndItem();
|
||||
}
|
||||
|
||||
let it = pedalboard.itemsGenerator();
|
||||
if (!selectedId) return null;
|
||||
while (true) {
|
||||
@@ -297,7 +308,7 @@ export const MainPage =
|
||||
}
|
||||
getSelectedUri(): string {
|
||||
let pedalboardItem = this.getSelectedPedalboardItem();
|
||||
if (pedalboardItem == null) return "";
|
||||
if (pedalboardItem === null) return "";
|
||||
return pedalboardItem.uri;
|
||||
}
|
||||
titleBar(pedalboardItem: PedalboardItem | null): React.ReactNode {
|
||||
@@ -311,7 +322,13 @@ export const MainPage =
|
||||
title = "";
|
||||
} else if (pedalboardItem.isSplit()) {
|
||||
title = "Split";
|
||||
} else {
|
||||
} else if (pedalboardItem.isSyntheticItem()) {
|
||||
title = pedalboardItem.pluginName ?? "#error";
|
||||
author = "";
|
||||
presetsUri = "";
|
||||
pluginUri = "";
|
||||
}
|
||||
else {
|
||||
let uiPlugin = this.model.getUiPlugin(pedalboardItem.uri);
|
||||
if (!uiPlugin) {
|
||||
missing = true;
|
||||
@@ -327,46 +344,41 @@ export const MainPage =
|
||||
}
|
||||
}
|
||||
let classes = this.props.classes;
|
||||
if (missing)
|
||||
{
|
||||
if (missing) {
|
||||
return (
|
||||
<div style={{
|
||||
flex: "1 0 auto", overflow: "hidden", marginRight: 8,
|
||||
display: "flex", flexDirection: "row", flexWrap: "nowrap",
|
||||
alignItems: "center"
|
||||
}}>
|
||||
<div style={{ flex: "0 1 auto" }}>
|
||||
<span style={{ whiteSpace: "nowrap", color: "#800000" }}>
|
||||
<span className={classes.title}>{title}</span>
|
||||
</span>
|
||||
<div style={{
|
||||
flex: "1 0 auto", overflow: "hidden", marginRight: 8, minWidth: 0,
|
||||
display: "flex", flexDirection: "row", height: 48, flexWrap: "nowrap",
|
||||
alignItems: "center"
|
||||
}}>
|
||||
<div style={{ flex: "0 1 auto",minWidth: 0 }}>
|
||||
<span style={{ color: "#800000" }}>
|
||||
<span className={classes.title}>{title}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
|
||||
} else {
|
||||
return (
|
||||
<div style={{
|
||||
flex: "1 0 auto", overflow: "hidden", marginRight: 8,
|
||||
display: "flex", flexDirection: "row", flexWrap: "nowrap",
|
||||
alignItems: "center"
|
||||
}}>
|
||||
<div style={{ flex: "0 1 auto" }}>
|
||||
<span style={{ whiteSpace: "nowrap" }}>
|
||||
return (
|
||||
<div style={{
|
||||
flex: "1 1 auto", minWidth: 0,overflow: "hidden", marginRight: 8,
|
||||
display: "flex", flexDirection: "row", height: 48, flexWrap: "nowrap",
|
||||
alignItems: "center"
|
||||
}}>
|
||||
<div style={{ flex: "0 1 auto", minWidth: 0,overflow: "hidden",textOverflow: "ellipsis" }}>
|
||||
<span className={classes.title}>{title}</span>
|
||||
</span>
|
||||
<span style={{ whiteSpace: "nowrap" }}>
|
||||
<span className={classes.author}>{author}</span>
|
||||
</span>
|
||||
</div>
|
||||
<div style={{ flex: "0 0 auto", verticalAlign: "center" }}>
|
||||
<PluginInfoDialog plugin_uri={pluginUri} />
|
||||
</div>
|
||||
<div style={{ flex: "0 0 auto" }}>
|
||||
<PluginPresetSelector pluginUri={presetsUri} instanceId={pedalboardItem?.instanceId ?? 0}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ flex: "0 0 auto", verticalAlign: "center" }}>
|
||||
<PluginInfoDialog plugin_uri={pluginUri} />
|
||||
</div>
|
||||
<div style={{ flex: "0 0 auto" }}>
|
||||
<PluginPresetSelector pluginUri={presetsUri} instanceId={pedalboardItem?.instanceId ?? 0}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -379,7 +391,9 @@ export const MainPage =
|
||||
let bypassVisible = false;
|
||||
let bypassChecked = false;
|
||||
let canDelete = false;
|
||||
let canAdd = false;
|
||||
let canInsert = false;
|
||||
let canAppend = false;
|
||||
let canLoad = true;
|
||||
let instanceId = -1;
|
||||
let missing = false;
|
||||
let pluginUri = "#error";
|
||||
@@ -388,13 +402,26 @@ export const MainPage =
|
||||
canDelete = pedalboard.canDeleteItem(pedalboardItem.instanceId);
|
||||
instanceId = pedalboardItem.instanceId;
|
||||
if (pedalboardItem.isEmpty()) {
|
||||
canAdd = true;
|
||||
canInsert = true;
|
||||
canAppend = true;
|
||||
|
||||
} else if (pedalboardItem.isStart()) {
|
||||
canAppend = true;
|
||||
canDelete = false;
|
||||
canLoad = false;
|
||||
} else if (pedalboardItem.isEnd()) {
|
||||
canInsert = true;
|
||||
canDelete = false;
|
||||
canLoad = false;
|
||||
} else if (pedalboardItem.isSplit()) {
|
||||
canAdd = true;
|
||||
canInsert = true;
|
||||
canAppend = true;
|
||||
canLoad = false;
|
||||
} else {
|
||||
pluginUri = pedalboardItem.uri;
|
||||
uiPlugin = this.model.getUiPlugin(pluginUri);
|
||||
canAdd = true;
|
||||
canInsert = true;
|
||||
canAppend = true;
|
||||
if (uiPlugin) {
|
||||
bypassVisible = true;
|
||||
bypassChecked = pedalboardItem.isEnabled;
|
||||
@@ -418,7 +445,7 @@ export const MainPage =
|
||||
<div className={classes.separator} />
|
||||
<div className={classes.controlToolBar}>
|
||||
<div style={{
|
||||
display: "flex", flexFlow: "row nowrap", alignItems: "center", justifyContent: "center",
|
||||
display: "flex", flexFlow: "row nowrap", alignItems: "center", justifyContent: "center", minWidth: 0,
|
||||
width: "100%", height: 48, paddingLeft: 16, paddingRight: 16
|
||||
}} >
|
||||
<div style={{ flex: "0 0 auto", width: 80 }} >
|
||||
@@ -432,7 +459,7 @@ export const MainPage =
|
||||
<div style={{ flex: "1 1 1px" }}>
|
||||
|
||||
</div>
|
||||
<div style={{ flex: "0 0 auto", display: canAdd ? "block" : "none", paddingRight: 8 }}>
|
||||
<div style={{ flex: "0 0 auto", display: (canInsert || canAppend) ? "block" : "none", paddingRight: 8 }}>
|
||||
<IconButton onClick={(e) => { this.onAddClick(e) }} size="large">
|
||||
<AddIcon />
|
||||
</IconButton>
|
||||
@@ -444,11 +471,11 @@ export const MainPage =
|
||||
onClose={() => this.handleAddClose()}
|
||||
TransitionComponent={Fade}
|
||||
>
|
||||
<MenuItem onClick={() => this.onInsertPedal(instanceId)}>Insert pedal</MenuItem>
|
||||
<MenuItem onClick={() => this.onAppendPedal(instanceId)}>Append pedal</MenuItem>
|
||||
{canInsert && (<MenuItem onClick={() => this.onInsertPedal(instanceId)}>Insert pedal</MenuItem>)}
|
||||
{canAppend && (<MenuItem onClick={() => this.onAppendPedal(instanceId)}>Append pedal</MenuItem>)}
|
||||
<Divider />
|
||||
<MenuItem onClick={() => this.onInsertSplit(instanceId)}>Insert split</MenuItem>
|
||||
<MenuItem onClick={() => this.onAppendSplit(instanceId)}>Append split</MenuItem>
|
||||
{canInsert && (<MenuItem onClick={() => this.onInsertSplit(instanceId)}>Insert split</MenuItem>)}
|
||||
{canAppend && (<MenuItem onClick={() => this.onAppendSplit(instanceId)}>Append split</MenuItem>)}
|
||||
</Menu>
|
||||
</div>
|
||||
<div style={{ flex: "0 0 auto", display: canDelete ? "block" : "none", paddingRight: 8 }}>
|
||||
@@ -464,9 +491,9 @@ export const MainPage =
|
||||
color="primary"
|
||||
size="small"
|
||||
onClick={this.onLoadClick}
|
||||
disabled={this.state.selectedPedal === -1 || (this.getSelectedPedalboardItem()?.isSplit() ?? true)}
|
||||
disabled={this.state.selectedPedal === -1 || (!canLoad) || (this.getSelectedPedalboardItem()?.isSplit() ?? true)}
|
||||
startIcon={<InputIcon />}
|
||||
style={{ borderRadius: 24, paddingLeft: 18, paddingRight: 18, textTransform: "none"}}
|
||||
style={{ textTransform: "none" }}
|
||||
>
|
||||
Load
|
||||
</Button>
|
||||
@@ -493,19 +520,19 @@ export const MainPage =
|
||||
<div className={horizontalScrollLayout ? classes.controlContentSmall : classes.controlContent}>
|
||||
{
|
||||
missing ? (
|
||||
<div style={{marginLeft: 100,marginTop: 20}}>
|
||||
<div style={{ marginLeft: 100, marginTop: 20 }}>
|
||||
<Typography variant="body1" paragraph={true}>Error: Plugin is not installed.</Typography>
|
||||
<Typography noWrap variant="body2" paragraph={true}>{pluginUri}</Typography>
|
||||
<Typography noWrap variant="body2" paragraph={true}>{pluginUri}</Typography>
|
||||
</div>
|
||||
|
||||
):
|
||||
(
|
||||
GetControlView(pedalboardItem)
|
||||
)
|
||||
) :
|
||||
(
|
||||
GetControlView(pedalboardItem)
|
||||
)
|
||||
}
|
||||
</div>
|
||||
<MidiBindingsDialog open={this.state.showMidiBindingsDialog}
|
||||
onClose={()=> this.setState({showMidiBindingsDialog: false} ) }
|
||||
onClose={() => this.setState({ showMidiBindingsDialog: false })}
|
||||
/>
|
||||
{
|
||||
(this.state.loadDialogOpen) && (
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
|
||||
import { createSvgIcon } from '@mui/material/utils';
|
||||
|
||||
const OldDeleteIcon = createSvgIcon(
|
||||
<path d="M20,4h-4.5l-1-1h-5l-1,1H4v2h1v13c0,1.1,0.9,2,2,2h10c1.1,0,2-0.9,2-2V6h1V4z M17,19H7V6h10V19z M9 8h2v8h-2z M13,8h2v8h-2z"/>,
|
||||
"Delete",
|
||||
);
|
||||
|
||||
export default OldDeleteIcon;
|
||||
@@ -24,7 +24,6 @@ import MidiBinding from './MidiBinding';
|
||||
const SPLIT_PEDALBOARD_ITEM_URI = "uri://two-play/pipedal/pedalboard#Split";
|
||||
const EMPTY_PEDALBOARD_ITEM_URI = "uri://two-play/pipedal/pedalboard#Empty";
|
||||
|
||||
|
||||
interface Deserializable<T> {
|
||||
deserialize(input: any): T;
|
||||
}
|
||||
@@ -92,6 +91,10 @@ export class PedalboardItem implements Deserializable<PedalboardItem> {
|
||||
return result;
|
||||
}
|
||||
|
||||
isSyntheticItem(): boolean {
|
||||
return this.instanceId === Pedalboard.START_CONTROL
|
||||
|| this.instanceId === Pedalboard.END_CONTROL;
|
||||
}
|
||||
getInstanceId() : number {
|
||||
if (this.instanceId === undefined)
|
||||
{
|
||||
@@ -105,6 +108,12 @@ export class PedalboardItem implements Deserializable<PedalboardItem> {
|
||||
isSplit(): boolean {
|
||||
return this.uri === SPLIT_PEDALBOARD_ITEM_URI;
|
||||
}
|
||||
isStart(): boolean {
|
||||
return this.uri === Pedalboard.START_PEDALBOARD_ITEM_URI;
|
||||
}
|
||||
isEnd(): boolean {
|
||||
return this.uri === Pedalboard.END_PEDALBOARD_ITEM_URI;
|
||||
}
|
||||
|
||||
getControl(key: string): ControlValue {
|
||||
for (let i = 0; i < this.controlValues.length; ++i) {
|
||||
@@ -186,7 +195,7 @@ export class PedalboardItem implements Deserializable<PedalboardItem> {
|
||||
controlValues: ControlValue[] = ControlValue.EmptyArray;
|
||||
midiBindings: MidiBinding[] = [];
|
||||
vstState: string = "";
|
||||
lv2State: any = {};
|
||||
lv2State: [boolean,any] = [false,{}]
|
||||
};
|
||||
|
||||
|
||||
@@ -253,8 +262,15 @@ export class PedalboardSplitItem extends PedalboardItem {
|
||||
|
||||
export class Pedalboard implements Deserializable<Pedalboard> {
|
||||
|
||||
static readonly START_CONTROL = -2; // synthetic PedalboardItem for input volume.
|
||||
static readonly END_CONTROL = -3; // synthetic PedalboardItem for output volume.
|
||||
static readonly START_PEDALBOARD_ITEM_URI = "uri://two-play/pipedal/pedalboard#Start";
|
||||
static readonly END_PEDALBOARD_ITEM_URI = "uri://two-play/pipedal/pedalboard#End";
|
||||
|
||||
deserialize(input: any): Pedalboard {
|
||||
this.name = input.name;
|
||||
this.input_volume_db = input.input_volume_db;
|
||||
this.output_volume_db = input.output_volume_db;
|
||||
this.items = PedalboardItem.deserializeArray(input.items);
|
||||
this.nextInstanceId = input.nextInstanceId ?? -1;
|
||||
return this;
|
||||
@@ -264,6 +280,8 @@ export class Pedalboard implements Deserializable<Pedalboard> {
|
||||
return new Pedalboard().deserialize(this);
|
||||
}
|
||||
name: string = "";
|
||||
input_volume_db: number = 0;
|
||||
output_volume_db: number = 0;
|
||||
items: PedalboardItem[] = [];
|
||||
nextInstanceId: number = -1;
|
||||
|
||||
@@ -316,6 +334,27 @@ export class Pedalboard implements Deserializable<Pedalboard> {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
makeStartItem(): PedalboardItem {
|
||||
let result = new PedalboardItem();
|
||||
result.pluginName = "Input";
|
||||
result.instanceId = Pedalboard.START_CONTROL;
|
||||
result.uri = Pedalboard.START_PEDALBOARD_ITEM_URI;
|
||||
result.isEnabled = true;
|
||||
result.controlValues = [new ControlValue("volume_db",this.input_volume_db)];
|
||||
return result;
|
||||
|
||||
}
|
||||
makeEndItem(): PedalboardItem {
|
||||
let result = new PedalboardItem();
|
||||
result.pluginName = "Output";
|
||||
result.instanceId = Pedalboard.END_CONTROL;
|
||||
result.uri = Pedalboard.END_PEDALBOARD_ITEM_URI;
|
||||
result.isEnabled = true;
|
||||
result.controlValues = [new ControlValue("volume_db",this.output_volume_db)];
|
||||
return result;
|
||||
|
||||
}
|
||||
|
||||
getItem(instanceId: number): PedalboardItem {
|
||||
let it = this.itemsGenerator();
|
||||
while (true)
|
||||
@@ -561,7 +600,7 @@ export class Pedalboard implements Deserializable<Pedalboard> {
|
||||
throw new PiPedalArgumentError("instanceId not found.");
|
||||
}
|
||||
}
|
||||
_addItem(items: PedalboardItem[], newItem: PedalboardItem, instanceId: number, append: boolean)
|
||||
private _addItem(items: PedalboardItem[], newItem: PedalboardItem, instanceId: number, append: boolean)
|
||||
{
|
||||
for (let i = 0; i < items.length; ++i)
|
||||
{
|
||||
|
||||
@@ -33,14 +33,16 @@ import Draggable from './Draggable'
|
||||
import Rect from './Rect';
|
||||
import {PiPedalStateError} from './PiPedalError';
|
||||
import Utility from './Utility'
|
||||
|
||||
|
||||
import {
|
||||
Pedalboard, PedalboardItem, PedalboardSplitItem, SplitType,
|
||||
Pedalboard, PedalboardItem, PedalboardSplitItem, SplitType
|
||||
} from './Pedalboard';
|
||||
|
||||
const START_PEDALBOARD_ITEM_URI = "uri://two-play/pipedal/pedalboard#Start";
|
||||
const END_PEDALBOARD_ITEM_URI = "uri://two-play/pipedal/pedalboard#End";
|
||||
const START_CONTROL = Pedalboard.START_CONTROL;
|
||||
const END_CONTROL = Pedalboard.END_CONTROL;
|
||||
|
||||
|
||||
const START_PEDALBOARD_ITEM_URI = Pedalboard.START_PEDALBOARD_ITEM_URI;
|
||||
const END_PEDALBOARD_ITEM_URI = Pedalboard.END_PEDALBOARD_ITEM_URI;
|
||||
|
||||
const ENABLED_CONNECTOR_COLOR = "#666";
|
||||
const DISABLED_CONNECTOR_COLOR = "#CCC";
|
||||
@@ -77,26 +79,6 @@ const pedalboardStyles = (theme: Theme) => createStyles({
|
||||
position: "relative",
|
||||
overflow: "visible",
|
||||
|
||||
},
|
||||
startItem: {
|
||||
position: "absolute",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
|
||||
width: CELL_WIDTH,
|
||||
height: CELL_HEIGHT
|
||||
|
||||
},
|
||||
endItem: {
|
||||
position: "absolute",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
|
||||
width: CELL_WIDTH,
|
||||
height: CELL_HEIGHT
|
||||
|
||||
},
|
||||
splitItem: {
|
||||
position: "absolute",
|
||||
@@ -125,7 +107,13 @@ const pedalboardStyles = (theme: Theme) => createStyles({
|
||||
right: 0,
|
||||
top: 0
|
||||
},
|
||||
|
||||
buttonDraggable: {
|
||||
display: "flex",
|
||||
alignItem: "center",
|
||||
justifyContent: "center",
|
||||
width: "100%",
|
||||
height: "100%"
|
||||
},
|
||||
pedalItem: {
|
||||
position: "absolute",
|
||||
width: CELL_WIDTH,
|
||||
@@ -150,6 +138,23 @@ const pedalboardStyles = (theme: Theme) => createStyles({
|
||||
border: "1pt #666 solid",
|
||||
borderRadius: 6
|
||||
},
|
||||
borderlessIconFrame: {
|
||||
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
|
||||
background: "transparent",
|
||||
marginLeft: (CELL_WIDTH - FRAME_SIZE) / 2,
|
||||
marginRight: (CELL_WIDTH - FRAME_SIZE) / 2,
|
||||
marginTop: (CELL_HEIGHT - FRAME_SIZE) / 2,
|
||||
marginBottom: (CELL_HEIGHT - FRAME_SIZE) / 2,
|
||||
width: FRAME_SIZE,
|
||||
height: FRAME_SIZE,
|
||||
border: "0pt #666 solid",
|
||||
borderRadius: 6
|
||||
},
|
||||
|
||||
pedalIcon: {
|
||||
width: 24,
|
||||
height: 24,
|
||||
@@ -632,7 +637,7 @@ const PedalboardView =
|
||||
if (bounds.height < TWO_ROW_HEIGHT) {
|
||||
|
||||
let extra = Math.floor((TWO_ROW_HEIGHT - Math.ceil(bounds.height))/2);
|
||||
this.offsetLayout_(layoutItems, Math.floor(-bounds.y + extra ));
|
||||
this.offsetLayout_(layoutItems, Math.floor(-bounds.y + extra/2 ));
|
||||
bounds.height += extra;
|
||||
|
||||
} else {
|
||||
@@ -677,7 +682,6 @@ const PedalboardView =
|
||||
|
||||
}
|
||||
|
||||
// XXX set keys on output objects !!
|
||||
renderConnector(output: ReactNode[], item: PedalLayout, enabled: boolean): void {
|
||||
// let classes = this.props.classes;
|
||||
let x_ = item.bounds.x + CELL_WIDTH / 2;
|
||||
@@ -688,14 +692,14 @@ const PedalboardView =
|
||||
|
||||
if (numberOfOutputs === 2) {
|
||||
output.push((
|
||||
<path d={svgPath} stroke={color} strokeWidth={SVG_STEREO_STROKE_WIDTH} />
|
||||
<path key={this.renderKey++} d={svgPath} stroke={color} strokeWidth={SVG_STEREO_STROKE_WIDTH} />
|
||||
));
|
||||
output.push((
|
||||
<path d={svgPath} stroke="white" strokeWidth={SVG_STROKE_WIDTH} />
|
||||
<path key={this.renderKey++} d={svgPath} stroke="white" strokeWidth={SVG_STROKE_WIDTH} />
|
||||
));
|
||||
} else if (numberOfOutputs === 1) {
|
||||
output.push((
|
||||
<path d={svgPath} stroke={color} strokeWidth={SVG_STROKE_WIDTH} />
|
||||
<path key={this.renderKey++} d={svgPath} stroke={color} strokeWidth={SVG_STROKE_WIDTH} />
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -718,18 +722,18 @@ const PedalboardView =
|
||||
let bottomStartPath = new SvgPathBuilder().moveTo(x_, y_).lineTo(x_, yBottom).lineTo(x_ + CELL_WIDTH, yBottom).toString();
|
||||
|
||||
if (item.numberOfInputs === 2 && item.topChildren[0].numberOfInputs === 2) {
|
||||
output.push((<path d={topStartPath} stroke={topColor} strokeWidth={SVG_STEREO_STROKE_WIDTH} />));
|
||||
output.push((<path d={topStartPath} stroke="white" strokeWidth={SVG_STROKE_WIDTH} />));
|
||||
output.push((<path key={this.renderKey++} d={topStartPath} stroke={topColor} strokeWidth={SVG_STEREO_STROKE_WIDTH} />));
|
||||
output.push((<path key={this.renderKey++} d={topStartPath} stroke="white" strokeWidth={SVG_STROKE_WIDTH} />));
|
||||
} else if (item.numberOfInputs !== 0 && item.topChildren[0].numberOfInputs !== 0) {
|
||||
output.push((<path d={topStartPath} stroke={topColor} strokeWidth={SVG_STROKE_WIDTH} />));
|
||||
output.push((<path key={this.renderKey++} d={topStartPath} stroke={topColor} strokeWidth={SVG_STROKE_WIDTH} />));
|
||||
}
|
||||
|
||||
if (item.numberOfInputs === 2 && item.bottomChildren[0].numberOfInputs === 2) {
|
||||
output.push((<path d={bottomStartPath} stroke={bottomColor} strokeWidth={SVG_STEREO_STROKE_WIDTH} />));
|
||||
output.push((<path d={bottomStartPath} stroke="white" strokeWidth={SVG_STROKE_WIDTH} />));
|
||||
output.push((<path key={this.renderKey++} d={bottomStartPath} stroke={bottomColor} strokeWidth={SVG_STEREO_STROKE_WIDTH} />));
|
||||
output.push((<path key={this.renderKey++} d={bottomStartPath} stroke="white" strokeWidth={SVG_STROKE_WIDTH} />));
|
||||
|
||||
} else if (item.numberOfInputs !== 0 && item.bottomChildren[0].numberOfInputs !== 0) {
|
||||
output.push((<path d={bottomStartPath} stroke={bottomColor} strokeWidth={SVG_STROKE_WIDTH} />));
|
||||
output.push((<path key={this.renderKey++} d={bottomStartPath} stroke={bottomColor} strokeWidth={SVG_STROKE_WIDTH} />));
|
||||
}
|
||||
|
||||
let lastTop = item.topChildren[item.topChildren.length - 1];
|
||||
@@ -813,26 +817,26 @@ const PedalboardView =
|
||||
// display stereo strokes with cutoff line.
|
||||
if (firstPathStereo) {
|
||||
output.push((
|
||||
<path d={firstPath} stroke={firstPathColor} strokeWidth={SVG_STEREO_STROKE_WIDTH} />
|
||||
<path key={this.renderKey++} d={firstPath} stroke={firstPathColor} strokeWidth={SVG_STEREO_STROKE_WIDTH} />
|
||||
));
|
||||
output.push((
|
||||
<path d={firstPath} stroke="white" strokeWidth={SVG_STROKE_WIDTH} />
|
||||
<path key={this.renderKey++} d={firstPath} stroke="white" strokeWidth={SVG_STROKE_WIDTH} />
|
||||
));
|
||||
} else if (!firstPathAbsent) {
|
||||
output.push((
|
||||
<path d={firstPath} stroke={firstPathColor} strokeWidth={SVG_STROKE_WIDTH} />
|
||||
<path key={this.renderKey++} d={firstPath} stroke={firstPathColor} strokeWidth={SVG_STROKE_WIDTH} />
|
||||
));
|
||||
}
|
||||
if (secondPathStereo) {
|
||||
output.push((
|
||||
<path d={secondPath} stroke={secondPathColor} strokeWidth={SVG_STEREO_STROKE_WIDTH} />
|
||||
<path key={this.renderKey++} d={secondPath} stroke={secondPathColor} strokeWidth={SVG_STEREO_STROKE_WIDTH} />
|
||||
));
|
||||
output.push((
|
||||
<path d={secondPath} stroke="white" strokeWidth={SVG_STROKE_WIDTH} />
|
||||
<path key={this.renderKey++} d={secondPath} stroke="white" strokeWidth={SVG_STROKE_WIDTH} />
|
||||
));
|
||||
} else if (!secondPathAbsent) {
|
||||
output.push((
|
||||
<path d={secondPath} stroke={secondPathColor} strokeWidth={SVG_STROKE_WIDTH} />
|
||||
<path key={this.renderKey++} d={secondPath} stroke={secondPathColor} strokeWidth={SVG_STROKE_WIDTH} />
|
||||
));
|
||||
}
|
||||
|
||||
@@ -840,32 +844,32 @@ const PedalboardView =
|
||||
// stereo strokes merge.
|
||||
if (firstPathStereo) {
|
||||
output.push((
|
||||
<path d={firstPath} stroke={firstPathColor} strokeWidth={SVG_STEREO_STROKE_WIDTH} />
|
||||
<path key={this.renderKey++} d={firstPath} stroke={firstPathColor} strokeWidth={SVG_STEREO_STROKE_WIDTH} />
|
||||
));
|
||||
} else if (!firstPathAbsent) {
|
||||
output.push((
|
||||
<path d={firstPath} stroke={firstPathColor} strokeWidth={SVG_STROKE_WIDTH} />
|
||||
<path key={this.renderKey++} d={firstPath} stroke={firstPathColor} strokeWidth={SVG_STROKE_WIDTH} />
|
||||
));
|
||||
}
|
||||
if (secondPathStereo) {
|
||||
output.push((
|
||||
<path d={secondPath} stroke={secondPathColor} strokeWidth={SVG_STEREO_STROKE_WIDTH} />
|
||||
<path key={this.renderKey++} d={secondPath} stroke={secondPathColor} strokeWidth={SVG_STEREO_STROKE_WIDTH} />
|
||||
));
|
||||
} else if (!secondPathAbsent) {
|
||||
output.push((
|
||||
<path d={secondPath} stroke={secondPathColor} strokeWidth={SVG_STROKE_WIDTH} />
|
||||
<path key={this.renderKey++} d={secondPath} stroke={secondPathColor} strokeWidth={SVG_STROKE_WIDTH} />
|
||||
));
|
||||
}
|
||||
|
||||
// draw stereo inner lines.
|
||||
if (firstPathStereo) {
|
||||
output.push((
|
||||
<path d={firstPath} stroke="white" strokeWidth={SVG_STROKE_WIDTH} />
|
||||
<path key={this.renderKey++} d={firstPath} stroke="white" strokeWidth={SVG_STROKE_WIDTH} />
|
||||
));
|
||||
}
|
||||
if (secondPathStereo) {
|
||||
output.push((
|
||||
<path d={secondPath} stroke="white" strokeWidth={SVG_STROKE_WIDTH} />
|
||||
<path key={this.renderKey++} d={secondPath} stroke="white" strokeWidth={SVG_STROKE_WIDTH} />
|
||||
));
|
||||
|
||||
}
|
||||
@@ -874,10 +878,10 @@ const PedalboardView =
|
||||
{
|
||||
// stereo output of L/R splitter
|
||||
output.push((
|
||||
<path d={thirdPath} stroke={secondPathColor} strokeWidth={SVG_STEREO_STROKE_WIDTH} />
|
||||
<path key={this.renderKey++} d={thirdPath} stroke={secondPathColor} strokeWidth={SVG_STEREO_STROKE_WIDTH} />
|
||||
));
|
||||
output.push((
|
||||
<path d={thirdPath} stroke="white" strokeWidth={SVG_STROKE_WIDTH} />
|
||||
<path key={this.renderKey++} d={thirdPath} stroke="white" strokeWidth={SVG_STROKE_WIDTH} />
|
||||
));
|
||||
|
||||
|
||||
@@ -898,25 +902,24 @@ const PedalboardView =
|
||||
throw new PiPedalStateError("scroll container not found.");
|
||||
}
|
||||
|
||||
pedalButton(instanceId: number, iconUrl: string, draggable: boolean, enabled: boolean): ReactNode {
|
||||
pedalButton(instanceId: number, iconUrl: string, draggable: boolean, enabled: boolean,hasBorder: boolean = true): ReactNode {
|
||||
let classes = this.props.classes;
|
||||
return (
|
||||
<div className={classes.iconFrame} onContextMenu={(e) => { e.preventDefault(); }}>
|
||||
<div className={hasBorder? classes.iconFrame : classes.borderlessIconFrame} onContextMenu={(e) => { e.preventDefault(); }}>
|
||||
|
||||
<ButtonBase style={{ width: "100%", height: "100%" }}
|
||||
onClick={(e) => { this.onItemClick(e, instanceId); }}
|
||||
onDoubleClick={(e: SyntheticEvent) => { this.onItemDoubleClick(e, instanceId); }}
|
||||
onContextMenu={(e: SyntheticEvent) => { this.onItemLongClick(e, instanceId); }}
|
||||
>
|
||||
<SelectHoverBackground selected={instanceId === this.props.selectedId} showHover={true} />
|
||||
<Draggable draggable={draggable} getScrollContainer={() => this.getScrollContainer()}
|
||||
onDragEnd={(x, y) => { this.onDragEnd(instanceId, x, y) }}
|
||||
>
|
||||
<div style={{ width: "100%", height: "100%" }}>
|
||||
<SelectHoverBackground selected={instanceId === this.props.selectedId} showHover={true} borderRadius={6} >
|
||||
<Draggable draggable={draggable} getScrollContainer={() => this.getScrollContainer()}
|
||||
onDragEnd={(x, y) => { this.onDragEnd(instanceId, x, y) }}
|
||||
>
|
||||
<img src={iconUrl} className={classes.pedalIcon} alt="Pedal" draggable={false}
|
||||
style={{opacity: enabled? 0.99: 0.6}} />
|
||||
</div>
|
||||
</Draggable>
|
||||
style={{opacity: enabled? 0.99: 0.6}} />
|
||||
</Draggable>
|
||||
</SelectHoverBackground>
|
||||
</ButtonBase>
|
||||
</div>
|
||||
);
|
||||
@@ -945,6 +948,7 @@ const PedalboardView =
|
||||
let outputs: ReactNode[] = [];
|
||||
this.renderConnectors(outputs, layoutChain, true, false);
|
||||
return (
|
||||
<div style={{width: layoutSize.width, height: layoutSize.height, overflow: "hidden"}}>
|
||||
<svg width={layoutSize.width} height={layoutSize.height}
|
||||
xmlns="http://www.w3.org/2000/svg" viewBox={"0 0 " + layoutSize.width + " " + layoutSize.height}>
|
||||
<g fill="none">
|
||||
@@ -954,6 +958,7 @@ const PedalboardView =
|
||||
</g>
|
||||
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
|
||||
}
|
||||
@@ -973,19 +978,25 @@ const PedalboardView =
|
||||
let item = v.value;
|
||||
switch (item.uri) {
|
||||
case START_PEDALBOARD_ITEM_URI:
|
||||
result.push(<div className={classes.startItem} style={{ left: item.bounds.x, top: item.bounds.y }} >
|
||||
<img src={item.iconUrl} className={classes.pedalIcon} alt="" draggable={false} />
|
||||
result.push(<div key={this.renderKey++} className={classes.splitItem} style={{ left: item.bounds.x, top: item.bounds.y, width: item.bounds.width }} >
|
||||
<div className={classes.splitStart} >
|
||||
|
||||
{this.pedalButton(START_CONTROL, item.iconUrl, false,true,false)}
|
||||
</div>
|
||||
</div>);
|
||||
break;
|
||||
case END_PEDALBOARD_ITEM_URI:
|
||||
result.push(<div className={classes.endItem} style={{ left: item.bounds.x, top: item.bounds.y }} >
|
||||
<img src={item.iconUrl} className={classes.pedalIcon} alt="" draggable={false} />
|
||||
result.push(<div key={this.renderKey++} className={classes.splitItem} style={{ left: item.bounds.x, top: item.bounds.y, width: item.bounds.width }} >
|
||||
<div className={classes.splitStart} >
|
||||
|
||||
{this.pedalButton(END_CONTROL, item.iconUrl, false,true,false)}
|
||||
</div>
|
||||
</div>);
|
||||
break;
|
||||
default:
|
||||
if (item.isSplitter()) {
|
||||
|
||||
result.push(<div className={classes.splitItem} style={{ left: item.bounds.x, top: item.bounds.y, width: item.bounds.width }} >
|
||||
result.push(<div key={this.renderKey++} className={classes.splitItem} style={{ left: item.bounds.x, top: item.bounds.y, width: item.bounds.width }} >
|
||||
<div className={classes.splitStart} >
|
||||
{this.pedalButton(item.pedalItem?.instanceId ?? -1, this.getSplitterIcon(item), false,true)}
|
||||
</div>
|
||||
@@ -993,7 +1004,7 @@ const PedalboardView =
|
||||
|
||||
} else {
|
||||
result.push(
|
||||
<div style={{
|
||||
<div key={this.renderKey++} style={{
|
||||
display: "flex", justifyContent: "flex-start", alignItems: "flex-start",
|
||||
position: "absolute", left: item.bounds.x, width: CELL_WIDTH, top: item.bounds.bottom - 12, paddingLeft: 2, paddingRight: 2
|
||||
}}>
|
||||
@@ -1002,7 +1013,7 @@ const PedalboardView =
|
||||
>{item.pedalItem?.pluginName}</Typography>
|
||||
</div>
|
||||
)
|
||||
result.push(<div className={classes.pedalItem} style={{ left: item.bounds.x, top: item.bounds.y }} >
|
||||
result.push(<div key={this.renderKey++} className={classes.pedalItem} style={{ left: item.bounds.x, top: item.bounds.y }} >
|
||||
{this.pedalButton(item.pedalItem?.instanceId ?? -1, item.iconUrl, !item.isEmpty(), item.pedalItem?.isEnabled ?? false)}
|
||||
|
||||
</div>);
|
||||
@@ -1151,10 +1162,10 @@ const PedalboardView =
|
||||
}
|
||||
|
||||
currentLayout?: PedalLayout[];
|
||||
|
||||
private renderKey: number = 0;
|
||||
render() {
|
||||
const { classes } = this.props;
|
||||
|
||||
this.renderKey = 0;
|
||||
let layoutChain = makeChain(this.model, this.state.pedalboard?.items);
|
||||
let start = PedalLayout.Start();
|
||||
let end = PedalLayout.End();
|
||||
|
||||
+182
-151
@@ -38,6 +38,7 @@ import GovernorSettings from './GovernorSettings';
|
||||
import WifiChannel from './WifiChannel';
|
||||
import AlsaDeviceInfo from './AlsaDeviceInfo';
|
||||
import { AndroidHostInterface, FakeAndroidHost } from './AndroidHost';
|
||||
import Xxx from './XxxSnippet';
|
||||
|
||||
|
||||
export enum State {
|
||||
@@ -338,153 +339,6 @@ export interface FavoritesList {
|
||||
[url: string]: boolean;
|
||||
}
|
||||
|
||||
// export interface PiPedalModel {
|
||||
// clientId: number;
|
||||
// countryCodes: Object;
|
||||
// debug: boolean;
|
||||
// serverVersion?: PiPedalVersion;
|
||||
// errorMessage: ObservableProperty<string>;
|
||||
// alertMessage: ObservableProperty<string>;
|
||||
// showStatusMonitor: ObservableProperty<boolean>;
|
||||
// setShowStatusMonitor(show: boolean): void;
|
||||
// state: ObservableProperty<State>;
|
||||
// visibilityState: ObservableProperty<VisibilityState>;
|
||||
|
||||
// ui_plugins: ObservableProperty<UiPlugin[]>;
|
||||
// plugin_classes: ObservableProperty<PluginClass>;
|
||||
// jackConfiguration: ObservableProperty<JackConfiguration>;
|
||||
// jackSettings: ObservableProperty<JackChannelSelection>;
|
||||
// banks: ObservableProperty<BankIndex>;
|
||||
// jackServerSettings: ObservableProperty<JackServerSettings>;
|
||||
// wifiConfigSettings: ObservableProperty<WifiConfigSettings>;
|
||||
// wifiDirectConfigSettings: ObservableProperty<WifiDirectConfigSettings>;
|
||||
// governorSettings: ObservableProperty<GovernorSettings>;
|
||||
// favorites: ObservableProperty<FavoritesList>;
|
||||
|
||||
// presets: ObservableProperty<PresetIndex>;
|
||||
// systemMidiBindings: ObservableProperty<MidiBinding[]>;
|
||||
|
||||
|
||||
// zoomedUiControl: ObservableProperty<ZoomedControlInfo | undefined>;
|
||||
|
||||
// setJackServerSettings(jackServerSettings: JackServerSettings): void;
|
||||
// setMidiBinding(instanceId: number, midiBinding: MidiBinding): void;
|
||||
|
||||
|
||||
// getUiPlugin(uri: string): UiPlugin | null;
|
||||
|
||||
// pedalboard: ObservableProperty<Pedalboard>;
|
||||
// loadPedalboardPlugin(itemId: number, selectedUri: string): number;
|
||||
|
||||
// setPedalboardControl(instanceId: number, key: string, value: number): void;
|
||||
// setPedalboardItemEnabled(instanceId: number, value: boolean): void;
|
||||
// previewPedalboardValue(instanceId: number, key: string, value: number): void;
|
||||
// deletePedalboardPedal(instanceId: number): number | null;
|
||||
|
||||
// movePedalboardItem(fromInstanceId: number, toInstanceId: number): void;
|
||||
// movePedalboardItemToStart(fromInstanceId: number): void;
|
||||
// movePedalboardItemToEnd(fromInstanceId: number): void;
|
||||
// movePedalboardItemBefore(fromInstanceId: number, toInstanceId: number): void;
|
||||
// movePedalboardItemAfter(fromInstanceId: number, toInstanceId: number): void;
|
||||
|
||||
// addPedalboardItem(instanceId: number, append: boolean): number;
|
||||
// addPedalboardSplitItem(instanceId: number, append: boolean): number;
|
||||
// setPedalboardItemEmpty(instanceId: number): number;
|
||||
|
||||
// saveCurrentPreset(): void;
|
||||
// saveCurrentPresetAs(newName: string): void;
|
||||
|
||||
// saveCurrentPluginPresetAs(pluginInstanceId: number, newName: string): void;
|
||||
|
||||
// loadPreset(instanceId: number): void;
|
||||
// updatePresets(presets: PresetIndex): Promise<void>;
|
||||
// deletePresetItem(instanceId: number): Promise<number>;
|
||||
// renamePresetItem(instanceId: number, name: string): Promise<void>;
|
||||
// copyPreset(fromId: number, toId: number): Promise<number>;
|
||||
// duplicatePreset(instanceId: number): Promise<number>;
|
||||
|
||||
// moveBank(from: number, to: number): Promise<void>;
|
||||
// deleteBankItem(instanceId: number): Promise<number>;
|
||||
|
||||
|
||||
// showAlert(message: string): void;
|
||||
|
||||
// setJackSettings(jackSettings: JackChannelSelection): void;
|
||||
|
||||
// svgImgUrl(svgImage: string): string;
|
||||
|
||||
// removeVuSubscription(handle: VuSubscriptionHandle): void;
|
||||
// addVuSubscription(instanceId: number, vuChangedHandler: VuChangedHandler): VuSubscriptionHandle;
|
||||
|
||||
// monitorPort(instanceid: number, key: string, updateRateSeconds: number, onUpdated: (value: number) => void): MonitorPortHandle;
|
||||
// unmonitorPort(handle: MonitorPortHandle): void;
|
||||
|
||||
// getPatchProperty<Type = any>(instanceId: number, uri: string): Promise<Type>;
|
||||
// setPatchProperty(instanceId: number, uri: string, value: any): Promise<boolean>;
|
||||
|
||||
// renameBank(instanceId: number, newName: string): Promise<void>;
|
||||
// saveBankAs(instanceId: number, newName: string): Promise<number>;
|
||||
// openBank(bankId: number): Promise<void>;
|
||||
|
||||
// getJackStatus(): Promise<JackHostStatus>;
|
||||
|
||||
// getPluginPresets(uri: string): Promise<PluginUiPresets>;
|
||||
// loadPluginPreset(pluginInstanceId: number, presetInstanceId: number): void;
|
||||
// updatePluginPresets(uri: string, presets: PluginUiPresets): Promise<void>;
|
||||
// duplicatePluginPreset(uri: string, instanceId: number): Promise<number>;
|
||||
|
||||
// setSystemMidiBinding(instanceId: number, midiBinding: MidiBinding): void;
|
||||
|
||||
// shutdown(): Promise<void>;
|
||||
// restart(): Promise<void>;
|
||||
|
||||
// listenForMidiEvent(listenForControlsOnly: boolean, onComplete: (isNote: boolean, noteOrControl: number) => void): ListenHandle;
|
||||
// cancelListenForMidiEvent(listenHandle: ListenHandle): void;
|
||||
|
||||
// addControlValueChangeListener(instanceId: number, onValueChanged: ControlValueChangedHandler): ControlValueChangedHandle
|
||||
// removeControlValueChangeListener(handle: ControlValueChangedHandle): void;
|
||||
|
||||
// addPluginPresetsChangedListener(onPluginPresetsChanged: PluginPresetsChangedHandler): PluginPresetsChangedHandle;
|
||||
// removePluginPresetsChangedListener(handle: PluginPresetsChangedHandle): void;
|
||||
|
||||
// listenForAtomOutput(instanceId: number, onComplete: (instanceId: number, atomOutput: any) => void): ListenHandle;
|
||||
// cancelListenForAtomOutput(listenHandle: ListenHandle): void;
|
||||
|
||||
|
||||
|
||||
// download(targetType: string, isntanceId: number | string): void;
|
||||
|
||||
// uploadPreset(uploadPage: string, file: File, uploadAfter: number): Promise<number>;
|
||||
// uploadBank(file: File, uploadAfter: number): Promise<number>;
|
||||
|
||||
// setWifiConfigSettings(wifiConfigSettings: WifiConfigSettings): Promise<void>;
|
||||
// setWifiDirectConfigSettings(wifiDirectConfigSettings: WifiDirectConfigSettings): Promise<void>;
|
||||
// setGovernorSettings(governor: string): Promise<void>;
|
||||
|
||||
// getWifiChannels(countryIso3661: string): Promise<WifiChannel[]>;
|
||||
|
||||
// getAlsaDevices(): Promise<AlsaDeviceInfo[]>;
|
||||
|
||||
// zoomUiControl(sourceElement: HTMLElement, instanceId: number, uiControl: UiControl): void;
|
||||
|
||||
// onPreviousZoomedControl(): void;
|
||||
// onNextZoomedControl(): void;
|
||||
// clearZoomedControl(): void;
|
||||
|
||||
// setFavorite(pluginUrl: string, isFavorite: boolean): void;
|
||||
|
||||
// isAndroidHosted(): boolean;
|
||||
// showAndroidDonationActivity(): void;
|
||||
// getAndroidHostVersion(): string;
|
||||
// chooseNewDevice(): void;
|
||||
|
||||
// isOnboarding(): boolean;
|
||||
// setOnboarding(value: boolean): void;
|
||||
|
||||
|
||||
|
||||
// requestFileList(piPedalFileProperty: PiPedalFileProperty): Promise<string[]>;
|
||||
// };
|
||||
|
||||
export class PiPedalModel //implements PiPedalModel
|
||||
{
|
||||
@@ -608,6 +462,14 @@ export class PiPedalModel //implements PiPedalModel
|
||||
controlChangedBody.value,
|
||||
false // do NOT notify the server of the change.
|
||||
);
|
||||
}
|
||||
else if (message === "onOutputVolumeChanged") {
|
||||
let value = body as number;
|
||||
this._setOutputVolume(value,false);
|
||||
}
|
||||
else if (message === "onInputVolumeChanged") {
|
||||
let value = body as number;
|
||||
this._setInputVolume(value,false);
|
||||
} else if (message === "onLv2StateChanged") {
|
||||
let instanceId = body as number;
|
||||
this.onLv2StateChanged(instanceId);
|
||||
@@ -755,6 +617,7 @@ export class PiPedalModel //implements PiPedalModel
|
||||
}
|
||||
|
||||
|
||||
|
||||
requestPluginClasses(): Promise<boolean> {
|
||||
const myRequest = new Request(this.varRequest('plugin_classes.json'));
|
||||
return fetch(myRequest)
|
||||
@@ -881,7 +744,8 @@ export class PiPedalModel //implements PiPedalModel
|
||||
|
||||
}
|
||||
|
||||
maxUploadSize: number = 512 * 1024;
|
||||
maxFileUploadSize: number = 100000000;
|
||||
maxPresetUploadSize: number = 512 * 1024;
|
||||
debug: boolean = false;
|
||||
|
||||
|
||||
@@ -895,7 +759,7 @@ export class PiPedalModel //implements PiPedalModel
|
||||
)
|
||||
.then(data => {
|
||||
if (data.max_upload_size) {
|
||||
this.maxUploadSize = data.max_upload_size;
|
||||
this.maxPresetUploadSize = data.max_upload_size;
|
||||
}
|
||||
if (data.fakeAndroid) {
|
||||
this.androidHost = new FakeAndroidHost();
|
||||
@@ -1295,13 +1159,93 @@ export class PiPedalModel //implements PiPedalModel
|
||||
};
|
||||
this.webSocket?.send("setPatchProperty", body);
|
||||
}
|
||||
|
||||
setInputVolume(volume_db: number) : void {
|
||||
this._setInputVolume(volume_db,true);
|
||||
}
|
||||
previewInputVolume(volume_db: number) : void {
|
||||
nullCast(this.webSocket).send("previewInputVolume",volume_db);
|
||||
|
||||
}
|
||||
previewOutputVolume(volume_db: number) : void {
|
||||
nullCast(this.webSocket).send("previewOutputVolume",volume_db);
|
||||
|
||||
}
|
||||
|
||||
private _setInputVolume(volume_db: number, notifyServer: boolean) : void {
|
||||
let changed: boolean = false;
|
||||
|
||||
let pedalboard = this.pedalboard.get();
|
||||
if (pedalboard.input_volume_db != volume_db)
|
||||
{
|
||||
let newPedalboard = pedalboard.clone();
|
||||
newPedalboard.input_volume_db = volume_db;
|
||||
this.pedalboard.set(newPedalboard);
|
||||
changed = true;
|
||||
}
|
||||
if (changed)
|
||||
{
|
||||
if (notifyServer) {
|
||||
nullCast(this.webSocket).send("setInputVolume",volume_db);
|
||||
}
|
||||
|
||||
for (let i = 0; i < this._controlValueChangeItems.length; ++i) {
|
||||
let item = this._controlValueChangeItems[i];
|
||||
if (Pedalboard.START_CONTROL === item.instanceId) {
|
||||
item.onValueChanged("volume_db", volume_db);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setOutputVolume(volume_db: number, notifyServer: boolean) : void
|
||||
{
|
||||
this._setOutputVolume(volume_db,true);
|
||||
}
|
||||
|
||||
private _setOutputVolume(volume_db: number, notifyServer: boolean) : void {
|
||||
let changed: boolean = false;
|
||||
|
||||
let pedalboard = this.pedalboard.get();
|
||||
if (pedalboard.output_volume_db != volume_db)
|
||||
{
|
||||
let newPedalboard = pedalboard.clone();
|
||||
newPedalboard.output_volume_db = volume_db;
|
||||
this.pedalboard.set(newPedalboard);
|
||||
changed = true;
|
||||
}
|
||||
if (changed)
|
||||
{
|
||||
if (notifyServer) {
|
||||
nullCast(this.webSocket).send("setOutputVolume",volume_db);
|
||||
}
|
||||
for (let i = 0; i < this._controlValueChangeItems.length; ++i) {
|
||||
let item = this._controlValueChangeItems[i];
|
||||
if (Pedalboard.END_CONTROL === item.instanceId) {
|
||||
item.onValueChanged("volume_db", volume_db);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private _setPedalboardControlValue(instanceId: number, key: string, value: number, notifyServer: boolean): void {
|
||||
let pedalboard = this.pedalboard.get();
|
||||
if (pedalboard === undefined) throw new PiPedalStateError("Pedalboard not ready.");
|
||||
let newPedalboard = pedalboard.clone();
|
||||
|
||||
if (instanceId === Pedalboard.START_CONTROL && key == "volume_db")
|
||||
{
|
||||
this._setInputVolume(value,notifyServer);
|
||||
return;
|
||||
} else if (instanceId === Pedalboard.END_CONTROL)
|
||||
{
|
||||
this._setOutputVolume(value,notifyServer);
|
||||
return;
|
||||
}
|
||||
let item = newPedalboard.getItem(instanceId);
|
||||
let changed = item.setControlValue(key, value);
|
||||
|
||||
if (changed) {
|
||||
this.pedalboard.set(newPedalboard);
|
||||
if (notifyServer) {
|
||||
@@ -1427,6 +1371,16 @@ export class PiPedalModel //implements PiPedalModel
|
||||
// mouse is down. Don't update EVERYBODY, but we must change
|
||||
// the control on the running audio plugin.
|
||||
// TODO: respect "expensive" port attribute.
|
||||
if (instanceId === Pedalboard.START_CONTROL && key == "volume_db")
|
||||
{
|
||||
this.previewInputVolume(value);
|
||||
return;
|
||||
} else if (instanceId === Pedalboard.END_CONTROL)
|
||||
{
|
||||
this.previewOutputVolume(value);
|
||||
return;
|
||||
}
|
||||
|
||||
this._setServerControl("previewControl", instanceId, key, value);
|
||||
}
|
||||
|
||||
@@ -1642,6 +1596,12 @@ export class PiPedalModel //implements PiPedalModel
|
||||
.request<string[]>('requestFileList', piPedalFileProperty);
|
||||
}
|
||||
|
||||
deleteUserFile(fileName: string) : Promise<boolean>
|
||||
{
|
||||
return nullCast(this.webSocket).request<boolean>('deleteUserFile',fileName)
|
||||
}
|
||||
|
||||
|
||||
saveCurrentPluginPresetAs(pluginInstanceId: number, newName: string): Promise<number> {
|
||||
// default behaviour is to save after the currently selected preset.
|
||||
let request: any = {
|
||||
@@ -2101,10 +2061,66 @@ export class PiPedalModel //implements PiPedalModel
|
||||
window.open(url, "_blank");
|
||||
}
|
||||
|
||||
uploadFile(uploadPage: string, file: File, contentType: string = "application/octet-stream", abortController?: AbortController): Promise<string> {
|
||||
let result = new Promise<string>((resolve, reject) => {
|
||||
try {
|
||||
if (file.size > this.maxFileUploadSize) {
|
||||
reject("File is too large.");
|
||||
}
|
||||
let url = this.varServerUrl + uploadPage;
|
||||
let parsedUrl = new URL(url);
|
||||
let fileNameOnly = file.name.split('/').pop()?.split('\\')?.pop();
|
||||
let query = parsedUrl.search;
|
||||
if (query.length === 0)
|
||||
{
|
||||
query += '?';
|
||||
} else {
|
||||
query += '&';
|
||||
}
|
||||
if (!fileNameOnly)
|
||||
{
|
||||
reject("Invalid filename.");
|
||||
}
|
||||
query += "filename=" + encodeURIComponent(fileNameOnly??"");
|
||||
parsedUrl.search = query;
|
||||
url = parsedUrl.toString();
|
||||
fetch(
|
||||
url,
|
||||
{
|
||||
method: "POST",
|
||||
body: file,
|
||||
headers: {
|
||||
'Content-Type': contentType,
|
||||
},
|
||||
signal: abortController?.signal
|
||||
}
|
||||
)
|
||||
.then((response: Response) => {
|
||||
if (!response.ok) {
|
||||
reject("Upload failed. " + response.statusText);
|
||||
return;
|
||||
} else {
|
||||
return response.json();
|
||||
}
|
||||
})
|
||||
.then((json) => {
|
||||
resolve(json as string);
|
||||
})
|
||||
.catch((error) => {
|
||||
reject("Upload failed. " + error);
|
||||
})
|
||||
;
|
||||
} catch (error) {
|
||||
reject("Upload failed. " + error);
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
uploadPreset(uploadPage: string, file: File, uploadAfter: number): Promise<number> {
|
||||
let result = new Promise<number>((resolve, reject) => {
|
||||
try {
|
||||
if (file.size > this.maxUploadSize) {
|
||||
if (file.size > this.maxPresetUploadSize) {
|
||||
reject("File is too large.");
|
||||
}
|
||||
let url = this.varServerUrl + uploadPage;
|
||||
@@ -2146,7 +2162,7 @@ export class PiPedalModel //implements PiPedalModel
|
||||
let result = new Promise<number>((resolve, reject) => {
|
||||
try {
|
||||
console.log("File: " + file.name + " Size: " + file.size);
|
||||
if (file.size > this.maxUploadSize) {
|
||||
if (file.size > this.maxPresetUploadSize) {
|
||||
reject("File is too large.");
|
||||
}
|
||||
let url = this.varServerUrl + "uploadBank";
|
||||
@@ -2387,6 +2403,11 @@ export class PiPedalModel //implements PiPedalModel
|
||||
|
||||
++ix;
|
||||
if (ix >= uiPlugin.controls.length) return;
|
||||
while (!uiPlugin.controls[ix].is_input)
|
||||
{
|
||||
++ix;
|
||||
if (ix >= uiPlugin.controls.length) return;
|
||||
}
|
||||
|
||||
this.zoomUiControl(currentUiControl.source, currentUiControl.instanceId, uiPlugin.controls[ix]);
|
||||
}
|
||||
@@ -2416,6 +2437,11 @@ export class PiPedalModel //implements PiPedalModel
|
||||
|
||||
--ix;
|
||||
if (ix < 0) return;
|
||||
while (!uiPlugin.controls[ix].is_input)
|
||||
{
|
||||
--ix;
|
||||
if (ix < 0) return;
|
||||
}
|
||||
|
||||
this.zoomUiControl(currentUiControl.source, currentUiControl.instanceId, uiPlugin.controls[ix]);
|
||||
}
|
||||
@@ -2471,6 +2497,11 @@ export class PiPedalModel //implements PiPedalModel
|
||||
}
|
||||
}
|
||||
|
||||
// returns the ID of the new preset.
|
||||
newPresetItem(createAfter: number): Promise<number>
|
||||
{
|
||||
return nullCast(this.webSocket).request<number>("newPreset");
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2022 Robin Davies
|
||||
// Copyright (c) 2022 Robin Davies
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
// this software and associated documentation files (the "Software"), to deal in
|
||||
|
||||
@@ -23,7 +23,7 @@ import { WithStyles } from '@mui/styles';
|
||||
import createStyles from '@mui/styles/createStyles';
|
||||
import withStyles from '@mui/styles/withStyles';
|
||||
import { PiPedalModel, PiPedalModelFactory } from './PiPedalModel';
|
||||
import { UiPlugin, UiControl, PiPedalFileProperty} from './Lv2Plugin';
|
||||
import { UiPlugin, UiControl, PiPedalFileProperty,ScalePoint} from './Lv2Plugin';
|
||||
import {
|
||||
Pedalboard, PedalboardItem, ControlValue
|
||||
} from './Pedalboard';
|
||||
@@ -37,6 +37,8 @@ import FullScreenIME from './FullScreenIME';
|
||||
import FilePropertyControl from './FilePropertyControl';
|
||||
import FilePropertyDialog from './FilePropertyDialog';
|
||||
import JsonAtom from './JsonAtom';
|
||||
import PluginOutputControl from './PluginOutputControl';
|
||||
import Units from './Units';
|
||||
|
||||
|
||||
export const StandardItemSize = { width: 80, height: 110 };
|
||||
@@ -45,6 +47,37 @@ export const StandardItemSize = { width: 80, height: 110 };
|
||||
|
||||
const LANDSCAPE_HEIGHT_BREAK = 500;
|
||||
|
||||
|
||||
function makeIoPluginInfo(name: string, uri: string) : UiPlugin {
|
||||
let result = new UiPlugin();
|
||||
result.name = name;
|
||||
result.uri = uri;
|
||||
let volumeControl = new UiControl();
|
||||
volumeControl.name = "Volume";
|
||||
volumeControl.symbol = "volume_db";
|
||||
volumeControl.index = 0;
|
||||
volumeControl.is_input = true;
|
||||
|
||||
volumeControl.min_value = -60;
|
||||
volumeControl.max_value = 30;
|
||||
volumeControl.default_value = 0;
|
||||
volumeControl.units = Units.db;
|
||||
volumeControl.scale_points = [
|
||||
new ScalePoint().deserialize({label: "-INF",value: -60})
|
||||
];
|
||||
result.controls = [
|
||||
volumeControl
|
||||
];
|
||||
return result;
|
||||
}
|
||||
|
||||
let startPluginInfo: UiPlugin =
|
||||
makeIoPluginInfo("Input",Pedalboard.START_PEDALBOARD_ITEM_URI);
|
||||
|
||||
let endPluginInfo: UiPlugin =
|
||||
makeIoPluginInfo("Output",Pedalboard.END_PEDALBOARD_ITEM_URI);
|
||||
|
||||
|
||||
const styles = (theme: Theme) => createStyles({
|
||||
frame: {
|
||||
display: "block",
|
||||
@@ -255,6 +288,7 @@ const PluginControlView =
|
||||
}
|
||||
|
||||
|
||||
|
||||
onWindowSizeChanged(width: number, height: number): void {
|
||||
this.setState({ landscapeGrid: height < LANDSCAPE_HEIGHT_BREAK });
|
||||
}
|
||||
@@ -296,6 +330,13 @@ const PluginControlView =
|
||||
}
|
||||
makeStandardControl(uiControl: UiControl, controlValues: ControlValue[]): ReactNode {
|
||||
let symbol = uiControl.symbol;
|
||||
if (!uiControl.is_input)
|
||||
{
|
||||
return (
|
||||
<PluginOutputControl instanceId={this.props.instanceId} uiControl={uiControl} />
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
let controlValue: ControlValue | undefined = undefined;
|
||||
for (let i = 0; i < controlValues.length; ++i) {
|
||||
@@ -435,10 +476,26 @@ const PluginControlView =
|
||||
return result;
|
||||
}
|
||||
|
||||
static startPluginInfo: UiPlugin =
|
||||
makeIoPluginInfo("Input",Pedalboard.START_PEDALBOARD_ITEM_URI);
|
||||
|
||||
static endPluginInfo: UiPlugin =
|
||||
makeIoPluginInfo("Output",Pedalboard.END_PEDALBOARD_ITEM_URI);
|
||||
|
||||
|
||||
render(): ReactNode {
|
||||
let classes = this.props.classes;
|
||||
let pedalboardItem = this.model.pedalboard.get().getItem(this.props.instanceId);
|
||||
let pedalboardItem: PedalboardItem;
|
||||
let pedalboard = this.model.pedalboard.get();
|
||||
if (this.props.instanceId == Pedalboard.START_CONTROL)
|
||||
{
|
||||
pedalboardItem = pedalboard.makeStartItem();
|
||||
} else if (this.props.instanceId == Pedalboard.END_CONTROL)
|
||||
{
|
||||
pedalboardItem = pedalboard.makeEndItem();
|
||||
} else {
|
||||
pedalboardItem = pedalboard.getItem(this.props.instanceId);
|
||||
}
|
||||
|
||||
if (!pedalboardItem)
|
||||
return (<div className={classes.frame} ></div>);
|
||||
@@ -446,7 +503,18 @@ const PluginControlView =
|
||||
let controlValues = pedalboardItem.controlValues;
|
||||
|
||||
|
||||
let plugin: UiPlugin = nullCast(this.model.getUiPlugin(pedalboardItem.uri));
|
||||
let plugin: UiPlugin;
|
||||
if (pedalboardItem.isStart())
|
||||
{
|
||||
plugin = startPluginInfo;
|
||||
controlValues = [new ControlValue("volume_db",pedalboard.input_volume_db)];
|
||||
} else if (pedalboardItem.isEnd())
|
||||
{
|
||||
plugin = endPluginInfo;
|
||||
controlValues = [new ControlValue("volume_db",pedalboard.output_volume_db)];
|
||||
} else {
|
||||
plugin = nullCast(this.model.getUiPlugin(pedalboardItem.uri));
|
||||
}
|
||||
|
||||
|
||||
controlValues = this.filterNotOnGui(controlValues, plugin);
|
||||
|
||||
@@ -0,0 +1,252 @@
|
||||
// Copyright (c) 2023 Robin Davies
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
// this software and associated documentation files (the "Software"), to deal in
|
||||
// the Software without restriction, including without limitation the rights to
|
||||
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
// the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
// subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import { Theme } from '@mui/material/styles';
|
||||
import { WithStyles } from '@mui/styles';
|
||||
import createStyles from '@mui/styles/createStyles';
|
||||
import withStyles from '@mui/styles/withStyles';
|
||||
import { UiControl } from './Lv2Plugin';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { PiPedalModel, PiPedalModelFactory, MonitorPortHandle } from './PiPedalModel';
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
const styles = (theme: Theme) => createStyles({
|
||||
frame: {
|
||||
position: "relative",
|
||||
margin: "12px"
|
||||
},
|
||||
switchTrack: {
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
borderRadius: 14 / 2,
|
||||
zIndex: -1,
|
||||
transition: theme.transitions.create(['opacity', 'background-color'], {
|
||||
duration: theme.transitions.duration.shortest
|
||||
}),
|
||||
backgroundColor: theme.palette.secondary.main,
|
||||
opacity: theme.palette.mode === 'light' ? 0.38 : 0.3
|
||||
},
|
||||
displayValue: {
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 4,
|
||||
textAlign: "center",
|
||||
background: "white",
|
||||
color: "#666",
|
||||
// zIndex: -1,
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
export interface PluginOutputControlProps extends WithStyles<typeof styles> {
|
||||
uiControl: UiControl;
|
||||
instanceId: number;
|
||||
}
|
||||
type PluginOutputControlState = {
|
||||
hasValue: boolean;
|
||||
value: number;
|
||||
};
|
||||
|
||||
const PluginOutputControl =
|
||||
withStyles(styles, { withTheme: true })(
|
||||
class extends Component<PluginOutputControlProps, PluginOutputControlState>
|
||||
{
|
||||
|
||||
private model: PiPedalModel;
|
||||
private vuRef: React.RefObject<HTMLDivElement>;
|
||||
|
||||
|
||||
constructor(props: PluginOutputControlProps) {
|
||||
super(props);
|
||||
this.vuRef = React.createRef<HTMLDivElement>();
|
||||
this.state = {
|
||||
hasValue: false,
|
||||
value: 0
|
||||
};
|
||||
this.model = PiPedalModelFactory.getInstance();
|
||||
}
|
||||
|
||||
private mounted: boolean = false;
|
||||
componentWillUnmount() {
|
||||
if (super.componentWillUnmount) {
|
||||
super.componentWillUnmount();
|
||||
}
|
||||
this.mounted = false;
|
||||
this.unsubscribe();
|
||||
}
|
||||
componentDidMount(): void {
|
||||
this.mounted = true;
|
||||
this.subscribe();
|
||||
if (super.componentDidMount) {
|
||||
super.componentDidMount();
|
||||
}
|
||||
}
|
||||
|
||||
private VU_HEIGHT = 48-2;
|
||||
private animationHandle: number | undefined = undefined;
|
||||
|
||||
private updateValue(value: number) {
|
||||
if (this.vuRef.current) {
|
||||
let control = this.props.uiControl;
|
||||
let range = (value-control.min_value)/(control.max_value-control.min_value);
|
||||
let top = this.VU_HEIGHT-range*this.VU_HEIGHT;
|
||||
|
||||
if (!this.animationHandle)
|
||||
{
|
||||
this.animationHandle = requestAnimationFrame(
|
||||
()=> {
|
||||
if (this.vuRef.current)
|
||||
{
|
||||
this.vuRef.current.style.marginTop = top+"px";
|
||||
}
|
||||
this.animationHandle = undefined;
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
} else {
|
||||
if (this.mounted) {
|
||||
this.setState({ hasValue: true, value: value });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private monitorPortHandle?: MonitorPortHandle;
|
||||
|
||||
private subscribe(): void {
|
||||
this.unsubscribe();
|
||||
this.monitorPortHandle = this.model.monitorPort(
|
||||
this.props.instanceId,
|
||||
this.props.uiControl.symbol,
|
||||
1 / 15.0, // update rate.
|
||||
(value) => {
|
||||
this.updateValue(value);
|
||||
}
|
||||
);
|
||||
|
||||
}
|
||||
private unsubscribe(): void {
|
||||
if (this.monitorPortHandle) {
|
||||
this.model.unmonitorPort(this.monitorPortHandle)
|
||||
this.monitorPortHandle = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
isShortSelect(control: UiControl) {
|
||||
for (let scale_point of control.scale_points) {
|
||||
if (scale_point.label.length > 12) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
formatDisplayValue(uiControl: UiControl | undefined, value: number): string {
|
||||
if (!uiControl) return "";
|
||||
|
||||
return uiControl.formatDisplayValue(value);
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
let control: UiControl = this.props.uiControl;
|
||||
|
||||
|
||||
let text = "";
|
||||
if (this.state.hasValue) {
|
||||
let value = this.state.value;
|
||||
if (control.isOnOffSwitch()) {
|
||||
text = value === 0 ? "OFF" : "ON";
|
||||
} else {
|
||||
text = this.formatDisplayValue(control, value);
|
||||
}
|
||||
}
|
||||
|
||||
let isSelect = control.isSelect();
|
||||
|
||||
let item_width: number | undefined = isSelect ? 160 : 80;
|
||||
if (isSelect) {
|
||||
if (this.isShortSelect(control)) {
|
||||
item_width = 80;
|
||||
}
|
||||
}
|
||||
if (control.isDial()) {
|
||||
item_width = undefined;
|
||||
return (
|
||||
<div style={{ display: "flex", flexDirection: "column", width: item_width, margin: 8, height: 98 }}>
|
||||
{/* TITLE SECTION */}
|
||||
<div style={{ flex: "0 0 auto", width: "100%", marginBottom: 8, marginLeft: 0, marginRight: 0 }}>
|
||||
<Typography variant="caption" display="block" style={{
|
||||
width: "100%",
|
||||
textAlign: "center"
|
||||
}}> {control.name}</Typography>
|
||||
</div>
|
||||
{/* CONTROL SECTION */}
|
||||
|
||||
<div style={{ flex: "1 1 auto", display: "flex", justifyContent: "center", alignItems: "center", flexFlow: "row nowrap" }}>
|
||||
<div style={{ width: 6, height: 48, background: "#000", }}>
|
||||
<div style={{ height: 46, overflow: "hidden", position: "absolute", margin: 1 }}>
|
||||
<div ref={this.vuRef} style={{ width: 4, height: 44, background: "#0C0", }} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{/* LABEL/EDIT SECTION*, strictly a placeholder for visual alignment purposes.*/}
|
||||
<div style={{ flex: "0 0 auto", position: "relative", width: 30, height: 27 }}>
|
||||
|
||||
</div>
|
||||
</div >
|
||||
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div style={{ display: "flex", flexDirection: "column", width: item_width, margin: 8, height: 98 }}>
|
||||
{/* TITLE SECTION */}
|
||||
<div style={{ flex: "0 0 auto", width: "100%", marginBottom: 8, marginLeft: 0, marginRight: 0 }}>
|
||||
<Typography variant="caption" display="block" style={{
|
||||
width: "100%",
|
||||
textAlign: "left"
|
||||
}}> {control.name}</Typography>
|
||||
</div>
|
||||
{/* CONTROL SECTION */}
|
||||
|
||||
<div style={{ flex: "1 1 auto", display: "flex", justifyContent: "start", alignItems: "center", flexFlow: "row nowrap" }}>
|
||||
<Typography variant="caption" display="block" noWrap style={{ width: "100%" }}>
|
||||
{text}
|
||||
</Typography>
|
||||
</div>
|
||||
|
||||
{/* LABEL/EDIT SECTION*, strictly a placeholder for visual alignment purposes.*/}
|
||||
<div style={{ flex: "0 0 auto", position: "relative", width: 40, height: 27 }}>
|
||||
|
||||
</div>
|
||||
</div >
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export default PluginOutputControl;
|
||||
@@ -33,7 +33,7 @@ import ListItemText from '@mui/material/ListItemText';
|
||||
import Menu from '@mui/material/Menu';
|
||||
import MenuItem from '@mui/material/MenuItem';
|
||||
import Fade from '@mui/material/Fade';
|
||||
import UploadDialog from './UploadDialog';
|
||||
import UploadPresetDialog from './UploadPresetDialog';
|
||||
import {PluginUiPresets,PluginUiPreset} from './PluginPreset';
|
||||
|
||||
import SelectHoverBackground from './SelectHoverBackground';
|
||||
@@ -67,7 +67,7 @@ interface PluginPresetsDialogState {
|
||||
renameOpen: boolean;
|
||||
|
||||
moreMenuAnchorEl: HTMLElement | null;
|
||||
openUploadDialog: boolean;
|
||||
openUploadPresetDialog: boolean;
|
||||
|
||||
|
||||
};
|
||||
@@ -138,7 +138,7 @@ const PluginPresetsDialog = withStyles(styles, { withTheme: true })(
|
||||
selectedItem: -1,
|
||||
renameOpen: false,
|
||||
moreMenuAnchorEl: null,
|
||||
openUploadDialog: false
|
||||
openUploadPresetDialog: false
|
||||
|
||||
};
|
||||
|
||||
@@ -165,7 +165,7 @@ const PluginPresetsDialog = withStyles(styles, { withTheme: true })(
|
||||
}
|
||||
handleUploadPresets() {
|
||||
this.handleMoreClose();
|
||||
this.setState({ openUploadDialog: true });
|
||||
this.setState({ openUploadPresetDialog: true });
|
||||
|
||||
}
|
||||
handleMoreClose(): void {
|
||||
@@ -466,14 +466,14 @@ const PluginPresetsDialog = withStyles(styles, { withTheme: true })(
|
||||
</DraggableGrid>
|
||||
</div>
|
||||
</div>
|
||||
<UploadDialog
|
||||
<UploadPresetDialog
|
||||
title="Upload Plugin Presets"
|
||||
extension='.piPluginPresets'
|
||||
uploadPage='uploadPluginPresets'
|
||||
onUploaded={(instanceId) => {} }
|
||||
uploadAfter={this.state.selectedItem}
|
||||
open={this.state.openUploadDialog}
|
||||
onClose={() => { this.setState({ openUploadDialog: false }) }} />
|
||||
open={this.state.openUploadPresetDialog}
|
||||
onClose={() => { this.setState({ openUploadPresetDialog: false }) }} />
|
||||
|
||||
</DialogEx>
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ import ListItemText from '@mui/material/ListItemText';
|
||||
import Menu from '@mui/material/Menu';
|
||||
import MenuItem from '@mui/material/MenuItem';
|
||||
import Fade from '@mui/material/Fade';
|
||||
import UploadDialog from './UploadDialog';
|
||||
import UploadPresetDialog from './UploadPresetDialog';
|
||||
|
||||
import SelectHoverBackground from './SelectHoverBackground';
|
||||
import CloseIcon from '@mui/icons-material//Close';
|
||||
@@ -64,7 +64,7 @@ interface PresetDialogState {
|
||||
renameOpen: boolean;
|
||||
|
||||
moreMenuAnchorEl: HTMLElement | null;
|
||||
openUploadDialog: boolean;
|
||||
openUploadPresetDialog: boolean;
|
||||
|
||||
|
||||
};
|
||||
@@ -137,7 +137,7 @@ const PresetDialog = withStyles(styles, { withTheme: true })(
|
||||
selectedItem: presets.selectedInstanceId,
|
||||
renameOpen: false,
|
||||
moreMenuAnchorEl: null,
|
||||
openUploadDialog: false
|
||||
openUploadPresetDialog: false
|
||||
|
||||
};
|
||||
this.handlePresetsChanged = this.handlePresetsChanged.bind(this);
|
||||
@@ -162,7 +162,7 @@ const PresetDialog = withStyles(styles, { withTheme: true })(
|
||||
}
|
||||
handleUploadPreset() {
|
||||
this.handleMoreClose();
|
||||
this.setState({ openUploadDialog: true });
|
||||
this.setState({ openUploadPresetDialog: true });
|
||||
|
||||
}
|
||||
handleMoreClose(): void {
|
||||
@@ -445,14 +445,14 @@ const PresetDialog = withStyles(styles, { withTheme: true })(
|
||||
</DraggableGrid>
|
||||
</div>
|
||||
</div>
|
||||
<UploadDialog
|
||||
<UploadPresetDialog
|
||||
title='Upload preset'
|
||||
extension='.piPreset'
|
||||
uploadPage='uploadPreset'
|
||||
onUploaded={(instanceId) => this.setState({selectedItem: instanceId}) }
|
||||
uploadAfter={this.state.selectedItem}
|
||||
open={this.state.openUploadDialog}
|
||||
onClose={() => { this.setState({ openUploadDialog: false }) }} />
|
||||
open={this.state.openUploadPresetDialog}
|
||||
onClose={() => { this.setState({ openUploadPresetDialog: false }) }} />
|
||||
|
||||
</DialogEx>
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ import Fade from '@mui/material/Fade';
|
||||
import Divider from '@mui/material/Divider';
|
||||
import RenameDialog from './RenameDialog'
|
||||
import Select from '@mui/material/Select';
|
||||
import UploadDialog from './UploadDialog';
|
||||
import UploadPresetDialog from './UploadPresetDialog';
|
||||
|
||||
|
||||
interface PresetSelectorProps extends WithStyles<typeof styles> {
|
||||
@@ -55,7 +55,7 @@ interface PresetSelectorState {
|
||||
renameDialogDefaultName: string;
|
||||
renameDialogActionName: string;
|
||||
renameDialogOnOk?: (name: string) => void;
|
||||
openUploadDialog: boolean;
|
||||
openUploadPresetDialog: boolean;
|
||||
|
||||
}
|
||||
|
||||
@@ -100,7 +100,7 @@ const PresetSelector =
|
||||
renameDialogDefaultName: "",
|
||||
renameDialogActionName: "",
|
||||
renameDialogOnOk: undefined,
|
||||
openUploadDialog: false
|
||||
openUploadPresetDialog: false
|
||||
|
||||
|
||||
};
|
||||
@@ -127,7 +127,7 @@ const PresetSelector =
|
||||
this.handlePresetsMenuClose();
|
||||
e.preventDefault();
|
||||
|
||||
this.setState({ openUploadDialog: true });
|
||||
this.setState({ openUploadPresetDialog: true });
|
||||
|
||||
}
|
||||
|
||||
@@ -151,7 +151,7 @@ const PresetSelector =
|
||||
return this.model.saveCurrentPresetAs(newName);
|
||||
})
|
||||
.then((newInstanceId) => {
|
||||
|
||||
|
||||
})
|
||||
.catch((error) => {
|
||||
this.showError(error);
|
||||
@@ -179,6 +179,22 @@ const PresetSelector =
|
||||
})
|
||||
;
|
||||
}
|
||||
handlePresetsMenuNew(e: SyntheticEvent): void {
|
||||
this.handlePresetsMenuClose();
|
||||
e.stopPropagation();
|
||||
|
||||
let currentPresets = this.model.presets.get();
|
||||
let item = currentPresets.getItem(currentPresets.selectedInstanceId);
|
||||
if (item == null) return;
|
||||
let name = item.name;
|
||||
this.model.newPresetItem(currentPresets.selectedInstanceId)
|
||||
.then((instanceId) => {
|
||||
this.model.loadPreset(instanceId);
|
||||
})
|
||||
.catch((error) => {
|
||||
this.showError(error);
|
||||
});
|
||||
}
|
||||
|
||||
showError(error: string) {
|
||||
this.model.showAlert(error);
|
||||
@@ -282,13 +298,24 @@ const PresetSelector =
|
||||
let presets = this.state.presets;
|
||||
let classes = this.props.classes;
|
||||
return (
|
||||
<div style={{ marginLeft: 12, display: "flex", flexDirection: "row",
|
||||
justifyContent: "left", flexWrap: "nowrap", alignItems: "center", height: "100%", position: "relative" }}>
|
||||
<div style={{
|
||||
marginLeft: 12, display: "flex", flexDirection: "row",
|
||||
justifyContent: "left", flexWrap: "nowrap", alignItems: "center", height: "100%", position: "relative"
|
||||
}}>
|
||||
<div style={{ flex: "0 0 auto" }}>
|
||||
<IconButton
|
||||
style={{ flex: "0 0 auto", opacity: this.state.presets.presetChanged ? 1.0 : 0.0 }}
|
||||
onClick={(e) => { this.handleSave(); }}
|
||||
size="large">
|
||||
<SaveIconOutline style={{ opacity: 0.75 }} />
|
||||
</IconButton>
|
||||
</div>
|
||||
|
||||
<div style={{ flex: "1 1 auto", minWidth: 60, maxWidth: 300, position: "relative", paddingRight: 12 }} >
|
||||
{true ? (
|
||||
<Select variant="standard"
|
||||
className={classes.select}
|
||||
style={{ width: "100%", position: "relative", top: 0}} disabled={!this.state.enabled}
|
||||
style={{ width: "100%", position: "relative", top: 0 }} disabled={!this.state.enabled}
|
||||
onChange={(e, extra) => this.handleChange(e, extra)}
|
||||
onClose={(e) => this.handleSelectClose(e)}
|
||||
displayEmpty
|
||||
@@ -331,14 +358,6 @@ const PresetSelector =
|
||||
</ButtonBase>
|
||||
)}
|
||||
</div>
|
||||
<div style={{ flex: "0 0 auto" }}>
|
||||
<IconButton
|
||||
style={{ flex: "0 0 auto", opacity: this.state.presets.presetChanged ? 1.0 : 0.0 }}
|
||||
onClick={(e) => { this.handleSave(); }}
|
||||
size="large">
|
||||
<SaveIconOutline style={{ opacity: 0.75 }} />
|
||||
</IconButton>
|
||||
</div>
|
||||
<div style={{ flex: "0 0 auto" }}>
|
||||
<IconButton
|
||||
style={{ flex: "0 0 auto" }}
|
||||
@@ -356,6 +375,7 @@ const PresetSelector =
|
||||
<MenuItem onClick={(e) => this.handlePresetsMenuSave(e)}>Save preset</MenuItem>
|
||||
<MenuItem onClick={(e) => this.handlePresetsMenuSaveAs(e)}>Save preset as...</MenuItem>
|
||||
<MenuItem onClick={(e) => this.handlePresetsMenuRename(e)}>Rename...</MenuItem>
|
||||
<MenuItem onClick={(e) => this.handlePresetsMenuNew(e)}>New...</MenuItem>
|
||||
<Divider />
|
||||
<MenuItem onClick={(e) => { this.handleDownloadPreset(e); }} >Download preset</MenuItem>
|
||||
<MenuItem onClick={(e) => { this.handleUploadPreset(e) }}>Upload preset</MenuItem>
|
||||
@@ -369,14 +389,14 @@ const PresetSelector =
|
||||
acceptActionName={this.state.renameDialogActionName}
|
||||
onClose={() => this.handleRenameDialogClose()}
|
||||
onOk={(name: string) => this.handleRenameDialogOk(name)} />
|
||||
<UploadDialog
|
||||
<UploadPresetDialog
|
||||
title='Upload preset'
|
||||
extension='.piPreset'
|
||||
uploadPage='uploadPreset'
|
||||
onUploaded={(instanceId) => { this.model.loadPreset(instanceId); }}
|
||||
open={this.state.openUploadDialog}
|
||||
open={this.state.openUploadPresetDialog}
|
||||
uploadAfter={-1}
|
||||
onClose={() => { this.setState({ openUploadDialog: false }) }} />
|
||||
onClose={() => { this.setState({ openUploadPresetDialog: false }) }} />
|
||||
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
import { Component, SyntheticEvent } from 'react';
|
||||
import React,{ Component, SyntheticEvent } from 'react';
|
||||
import { Theme } from '@mui/material/styles';
|
||||
|
||||
|
||||
@@ -54,11 +54,12 @@ const styles = ({ palette }: Theme) => createStyles({
|
||||
interface HoverProps extends WithStyles<typeof styles> {
|
||||
selected: boolean;
|
||||
showHover?: boolean;
|
||||
borderRadius?: number;
|
||||
theme: Theme;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
interface HoverState {
|
||||
hovering: boolean;
|
||||
}
|
||||
|
||||
|
||||
@@ -69,44 +70,53 @@ export const SelectHoverBackground =
|
||||
constructor(props: HoverProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
hovering: false
|
||||
};
|
||||
this.selfRef = React.createRef<HTMLDivElement>();
|
||||
this.hoverElementRef = React.createRef<HTMLDivElement>();
|
||||
|
||||
this.handleMouseEnter = this.handleMouseEnter.bind(this);
|
||||
this.handleMouseOver = this.handleMouseOver.bind(this);
|
||||
this.handleMouseLeave = this.handleMouseLeave.bind(this);
|
||||
|
||||
}
|
||||
|
||||
handleMouseEnter(e: SyntheticEvent): void {
|
||||
if (this.props.showHover??true)
|
||||
{
|
||||
this.setHovering(true);
|
||||
handleMouseOver(e: SyntheticEvent): void {
|
||||
console.log("onMouseOver")
|
||||
if (this.props.showHover ?? true) {
|
||||
if (this.hoverElementRef.current)
|
||||
{
|
||||
this.hoverElementRef.current.style.display = "block";
|
||||
} else {
|
||||
console.log("No hoverElementRef")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
handleMouseLeave(e: SyntheticEvent): void {
|
||||
if (this.props.showHover??true)
|
||||
{
|
||||
this.setHovering(false);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
setHovering(value: boolean): void {
|
||||
if (this.state.hovering !== value) {
|
||||
this.setState({ hovering: value });
|
||||
if (this.props.showHover ?? true) {
|
||||
if (this.hoverElementRef.current)
|
||||
{
|
||||
this.hoverElementRef.current.style.display = "none";
|
||||
} else {
|
||||
console.log("No hoverElementRef")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private selfRef: React.RefObject<HTMLDivElement>;
|
||||
private hoverElementRef: React.RefObject<HTMLDivElement>;
|
||||
|
||||
render() {
|
||||
let classes = this.props.classes;
|
||||
let isSelected = this.props.selected;
|
||||
let hovering = this.state.hovering && (this.props.showHover??true);
|
||||
return (<div className={classes.frame}
|
||||
onMouseEnter={(e) => { this.handleMouseEnter(e); }} onMouseLeave={(e) => { this.handleMouseLeave(e) }}
|
||||
return (<div className={classes.frame} ref={this.selfRef}
|
||||
onMouseOver={(e) => { this.handleMouseOver(e); }} onMouseLeave={(e) => { this.handleMouseLeave(e) }}
|
||||
>
|
||||
<div className={classes.selected} style={{ display: (isSelected ? "block" : "none") }} />
|
||||
<div className={classes.hover} style={{ display: (hovering ? "block" : "none") }} />
|
||||
<div className={classes.selected} style={{ display: (isSelected ? "block" : "none"), borderRadius: this.props.borderRadius }} />
|
||||
<div ref={this.hoverElementRef} className={classes.hover} style={{ display: "none", borderRadius: this.props.borderRadius }} />
|
||||
{
|
||||
this.props.children
|
||||
}
|
||||
</div>);
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,414 @@
|
||||
// Copyright (c) 2022 Robin Davies
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
// this software and associated documentation files (the "Software"), to deal in
|
||||
// the Software without restriction, including without limitation the rights to
|
||||
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
// the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
// subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
import React from 'react';
|
||||
import Button from '@mui/material/Button';
|
||||
import { PiPedalModel, PiPedalModelFactory } from './PiPedalModel';
|
||||
|
||||
import DialogEx from './DialogEx';
|
||||
import DialogTitle from '@mui/material/DialogTitle';
|
||||
import DialogActions from '@mui/material/DialogActions';
|
||||
import DialogContent from '@mui/material/DialogContent';
|
||||
import ResizeResponsiveComponent from './ResizeResponsiveComponent';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { PiPedalFileProperty } from './Lv2Plugin';
|
||||
import SvgIcon from '@mui/material/SvgIcon';
|
||||
import ErrorIcon from '@mui/icons-material/Error';
|
||||
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline';
|
||||
import CircularProgress from '@mui/material/CircularProgress';
|
||||
|
||||
|
||||
// const PRESET_EXTENSION = ".piPreset";
|
||||
// const BANK_EXTENSION = ".piBank";
|
||||
// const PLUGIN_PRESETS_EXTENSION = ".piPluginPresets";
|
||||
|
||||
export interface UploadFileDialogProps {
|
||||
open: boolean,
|
||||
onClose: () => void,
|
||||
onUploaded: (fileName: string) => void,
|
||||
uploadPage: string,
|
||||
fileProperty: PiPedalFileProperty
|
||||
|
||||
};
|
||||
|
||||
enum FileUploadStatus {
|
||||
Ready,
|
||||
Uploading,
|
||||
Uploaded,
|
||||
Error,
|
||||
Cancelled
|
||||
}
|
||||
|
||||
let nextId = 0;
|
||||
|
||||
class FileUploadEntry {
|
||||
constructor(file: File) {
|
||||
this.id = nextId++;
|
||||
this.file = file;
|
||||
this.abortController = undefined;
|
||||
this.status = FileUploadStatus.Ready;
|
||||
this.statusMessage = "";
|
||||
}
|
||||
status: FileUploadStatus
|
||||
statusMessage: string;
|
||||
file: File;
|
||||
abortController?: AbortController;
|
||||
id: number;
|
||||
}
|
||||
|
||||
class FileUploadDisplayEntry {
|
||||
constructor(fileUploadEntry: FileUploadEntry) {
|
||||
this.name = fileUploadEntry.file.name;
|
||||
this.status = fileUploadEntry.status;
|
||||
this.statusMessage = fileUploadEntry.statusMessage;
|
||||
this.id = fileUploadEntry.id;
|
||||
}
|
||||
name: string;
|
||||
status: FileUploadStatus;
|
||||
statusMessage: string;
|
||||
id: number;
|
||||
};
|
||||
|
||||
export interface UploadFileDialogState {
|
||||
fullScreen: boolean;
|
||||
okEnabled: boolean;
|
||||
files: FileUploadDisplayEntry[];
|
||||
};
|
||||
|
||||
export default class UploadFileDialog extends ResizeResponsiveComponent<UploadFileDialogProps, UploadFileDialogState> {
|
||||
|
||||
model: PiPedalModel;
|
||||
|
||||
constructor(props: UploadFileDialogProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
fullScreen: false,
|
||||
okEnabled: true,
|
||||
files: []
|
||||
};
|
||||
this.model = PiPedalModelFactory.getInstance();
|
||||
|
||||
}
|
||||
mounted: boolean = false;
|
||||
|
||||
|
||||
|
||||
onWindowSizeChanged(width: number, height: number): void {
|
||||
this.setState({ fullScreen: height < 200 })
|
||||
}
|
||||
|
||||
|
||||
componentDidMount() {
|
||||
super.componentDidMount();
|
||||
this.mounted = true;
|
||||
|
||||
}
|
||||
componentWillUnmount() {
|
||||
super.componentWillUnmount();
|
||||
this.mounted = false;
|
||||
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: Readonly<UploadFileDialogProps>, prevState: Readonly<UploadFileDialogState>, snapshot?: any): void {
|
||||
if (prevProps.open !== this.props.open) {
|
||||
if (this.props.open) {
|
||||
this.uploadList = [];
|
||||
this.setState({ files: [], okEnabled: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleClose(): void {
|
||||
this.uploadList = [];
|
||||
this.setState({ files: [] });
|
||||
this.props.onClose();
|
||||
}
|
||||
|
||||
handleCancel() {
|
||||
for (let upload of this.uploadList) {
|
||||
if (upload.status === FileUploadStatus.Ready || upload.status === FileUploadStatus.Uploading) {
|
||||
upload.status = FileUploadStatus.Cancelled;
|
||||
upload.statusMessage = "Cancelled.";
|
||||
}
|
||||
if (upload.abortController) {
|
||||
upload.abortController.abort();
|
||||
upload.abortController = undefined;
|
||||
}
|
||||
|
||||
}
|
||||
this.handleClose();
|
||||
}
|
||||
handleOk(): void {
|
||||
this.props.onClose();
|
||||
}
|
||||
|
||||
updateUploadDisplayList() {
|
||||
let result: FileUploadDisplayEntry[] = [];
|
||||
let uploadsPending = false;
|
||||
for (let upload of this.uploadList) {
|
||||
result.push(new FileUploadDisplayEntry(upload));
|
||||
if (upload.status === FileUploadStatus.Ready || upload.status === FileUploadStatus.Uploading) {
|
||||
uploadsPending = true;
|
||||
}
|
||||
|
||||
}
|
||||
this.setState({ files: result, okEnabled: !uploadsPending });
|
||||
}
|
||||
|
||||
private uploading = false;
|
||||
private wantsScrollTo = false;
|
||||
|
||||
private uploadList: FileUploadEntry[] = [];
|
||||
|
||||
async uploadFiles(files: FileList) {
|
||||
for (let i = 0; i < files.length; ++i) {
|
||||
let file = files.item(i);
|
||||
if (file) {
|
||||
this.uploadList.push(new FileUploadEntry(file));
|
||||
}
|
||||
}
|
||||
this.wantsScrollTo = true;
|
||||
if (this.uploading) {
|
||||
this.updateUploadDisplayList();
|
||||
return;
|
||||
}
|
||||
this.uploading = true;
|
||||
|
||||
try {
|
||||
for (let i = 0; i < this.uploadList.length; ++i) {
|
||||
let upload = this.uploadList[i];
|
||||
if (upload.status !== FileUploadStatus.Ready) {
|
||||
continue;
|
||||
}
|
||||
upload.status = FileUploadStatus.Uploading;
|
||||
upload.statusMessage = "Uploading...";
|
||||
this.wantsScrollTo = true;
|
||||
this.updateUploadDisplayList();
|
||||
try {
|
||||
upload.abortController = new AbortController();
|
||||
let filename = await this.model.uploadFile(this.props.uploadPage, this.uploadList[i].file, "application/octet-stream", upload.abortController);
|
||||
this.props.onUploaded(filename);
|
||||
upload.status = FileUploadStatus.Uploaded;
|
||||
upload.statusMessage = "Uploaded.";
|
||||
upload.abortController = undefined;
|
||||
} catch (error: any) {
|
||||
// @ts-ignore: TS2367 // No overlap between FileUploadStatus.Updating and FileUploadStatus.Cancelled
|
||||
if (upload.status !== FileUploadStatus.Cancelled) {
|
||||
upload.status = FileUploadStatus.Error;
|
||||
if (error instanceof Error) {
|
||||
upload.statusMessage = (error as Error).message;
|
||||
} else if (typeof error === "string") {
|
||||
upload.statusMessage = error as string;
|
||||
} else {
|
||||
throw new Error("Unrecgnized exception type: " + error.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
this.model.showAlert(error + "");
|
||||
};
|
||||
this.updateUploadDisplayList();
|
||||
this.uploading = false;
|
||||
}
|
||||
handleDrop(e: React.DragEvent<HTMLDivElement>) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
e.currentTarget.style.background = "";
|
||||
|
||||
if (this.wantsTransfer(e.dataTransfer)) {
|
||||
this.uploadFiles(e.dataTransfer.files);
|
||||
}
|
||||
}
|
||||
|
||||
private wantsFile(file: File | null) {
|
||||
if (file === null) return false;
|
||||
return this.props.fileProperty.wantsFile(file.name);
|
||||
|
||||
}
|
||||
private wantsTransfer(fileList: DataTransfer): boolean {
|
||||
if (fileList.files.length === 0) return false;
|
||||
|
||||
console.log("File count: " + fileList.files.length);
|
||||
for (let i = 0; i < fileList.files.length; ++i) {
|
||||
let file = fileList.files[i];
|
||||
if (this.wantsFile(file)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
handleDragOver(e: React.DragEvent<HTMLDivElement>) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
e.currentTarget.style.background = "#D8D8D8";
|
||||
}
|
||||
|
||||
handleButtonSelect(e: React.ChangeEvent<HTMLInputElement>) {
|
||||
if (e.currentTarget.files) {
|
||||
this.uploadFiles(e.currentTarget.files);
|
||||
}
|
||||
}
|
||||
hasFileList() {
|
||||
return this.state.files.length !== 0;
|
||||
}
|
||||
|
||||
getIcon(status: FileUploadStatus) {
|
||||
let size = 14;
|
||||
|
||||
let style = { width: size, height: size, opacity: 0.75 };
|
||||
let top = 0;
|
||||
|
||||
switch (status) {
|
||||
case FileUploadStatus.Error:
|
||||
return (<div style={{ position: "relative", top: top }}><ErrorIcon color="error" style={style} /></div>);
|
||||
case FileUploadStatus.Uploaded:
|
||||
return (<div style={{ position: "relative", top: top }}><CheckCircleOutlineIcon color="success" style={style}/></div >);
|
||||
case FileUploadStatus.Uploading:
|
||||
return (<CircularProgress size={size } style={{ margin: 2 }} />);
|
||||
|
||||
default:
|
||||
return (<SvgIcon style={{ width: size, height: size, }} />);
|
||||
}
|
||||
}
|
||||
makeBrowserAcceptList() {
|
||||
let fileTypes = this.props.fileProperty.fileTypes;
|
||||
let result = "";
|
||||
for (let i = 0; i < fileTypes.length; ++i) {
|
||||
if (result.length !== 0) {
|
||||
result += ',';
|
||||
}
|
||||
result += fileTypes[i].fileExtension;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
|
||||
return (
|
||||
<DialogEx tag="UploadFileDialog" open={this.props.open} fullWidth onClose={() => this.handleClose()}
|
||||
fullScreen={this.state.fullScreen}
|
||||
style={{ userSelect: "none" }}
|
||||
>
|
||||
<DialogTitle >
|
||||
<div>
|
||||
<IconButton edge="start" color="inherit" onClick={() => { this.handleCancel(); }} aria-label="back"
|
||||
>
|
||||
<ArrowBackIcon fontSize="small" style={{ opacity: "0.6" }} />
|
||||
</IconButton>
|
||||
<Typography display="inline" >Upload</Typography>
|
||||
</div>
|
||||
</DialogTitle>
|
||||
<DialogContent style={{ paddingBottom: 0 }}>
|
||||
<div style={{
|
||||
width: "100%", height: 140, marginBottom: 0,
|
||||
border: "2px dashed #CCC",
|
||||
borderRadius: "10px",
|
||||
fontFamily: "Roboto",
|
||||
padding: 0
|
||||
}}
|
||||
onDragEnter={(e) => { this.handleDragOver(e); }}
|
||||
onDragLeave={(e) => { e.currentTarget.style.background = ""; e.preventDefault(); }}
|
||||
onDragOver={(e) => { this.handleDragOver(e); }}
|
||||
onDrop={(e) => {
|
||||
this.handleDrop(e);
|
||||
}}
|
||||
>
|
||||
{!this.hasFileList() && (
|
||||
<Typography noWrap color="textSecondary" align="center" display="block" variant="caption" style={{ width: "100%", marginTop: 20, verticalAlign: "middle" }} >Drop files here</Typography>
|
||||
)}
|
||||
{this.hasFileList() && (
|
||||
<div style={{ width: "100%", height: "100%", overflow: "auto" }}>
|
||||
<table style={{ margin: 8, }}>
|
||||
{
|
||||
this.state.files.map(
|
||||
(upload) => {
|
||||
let myRef: ((arg: HTMLDivElement) => void) | undefined = undefined;
|
||||
if (this.wantsScrollTo && upload.status === FileUploadStatus.Uploading) {
|
||||
myRef = (refElement: HTMLDivElement | null) => {
|
||||
if (refElement && this.wantsScrollTo) {
|
||||
let options: ScrollIntoViewOptions = {
|
||||
block: "center"
|
||||
};
|
||||
refElement.scrollIntoView(options);
|
||||
this.wantsScrollTo = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return (
|
||||
<tr>
|
||||
<td style={{ verticalAlign: "middle" }}>
|
||||
<div ref={myRef} style={{ display: "flex", flexFlow: "row nowrap", alignItems: "center" }}>
|
||||
{this.getIcon(upload.status)}
|
||||
<Typography variant="caption" noWrap display="block" style={{ marginLeft: 4 }}> {upload.name} </Typography>
|
||||
</div>
|
||||
</td>
|
||||
<td style={{ verticalAlign: "middle" }}>
|
||||
<Typography variant="caption" noWrap display="block" style={{ marginLeft: 8 }}> {upload.statusMessage} </Typography>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
|
||||
}
|
||||
)
|
||||
}
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
</div>
|
||||
</DialogContent >
|
||||
|
||||
<DialogActions >
|
||||
<Button
|
||||
component="label" color="primary" style={{ marginLeft: 16, width: 120, flex: "0 0 auto" }}
|
||||
>
|
||||
Select Files
|
||||
<input
|
||||
type="file"
|
||||
hidden
|
||||
accept={this.makeBrowserAcceptList()}
|
||||
multiple
|
||||
onChange={(e) => this.handleButtonSelect(e)}
|
||||
/>
|
||||
</Button>
|
||||
<div style={{ flex: "1 1 20px" }} />
|
||||
{
|
||||
this.state.okEnabled ?
|
||||
(
|
||||
<Button onClick={() => this.handleClose()} color="secondary">
|
||||
OK
|
||||
</Button>
|
||||
|
||||
) : (
|
||||
<Button onClick={() => this.handleCancel()} color="primary">
|
||||
Cancel
|
||||
</Button>
|
||||
|
||||
)
|
||||
}
|
||||
</DialogActions>
|
||||
</DialogEx>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -33,7 +33,7 @@ import Typography from '@mui/material/Typography';
|
||||
// const BANK_EXTENSION = ".piBank";
|
||||
// const PLUGIN_PRESETS_EXTENSION = ".piPluginPresets";
|
||||
|
||||
export interface UploadDialogProps {
|
||||
export interface UploadPresetDialogProps {
|
||||
open: boolean,
|
||||
onClose: () => void,
|
||||
uploadAfter: number,
|
||||
@@ -44,15 +44,15 @@ export interface UploadDialogProps {
|
||||
|
||||
};
|
||||
|
||||
export interface UploadDialogState {
|
||||
export interface UploadPresetDialogState {
|
||||
fullScreen: boolean;
|
||||
};
|
||||
|
||||
export default class UploadDialog extends ResizeResponsiveComponent<UploadDialogProps, UploadDialogState> {
|
||||
export default class UploadPresetDialog extends ResizeResponsiveComponent<UploadPresetDialogProps, UploadPresetDialogState> {
|
||||
|
||||
model: PiPedalModel;
|
||||
|
||||
constructor(props: UploadDialogProps) {
|
||||
constructor(props: UploadPresetDialogProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
fullScreen: false
|
||||
@@ -157,7 +157,7 @@ export default class UploadDialog extends ResizeResponsiveComponent<UploadDialog
|
||||
|
||||
|
||||
return (
|
||||
<DialogEx tag="UploadDialog" open={this.props.open} fullWidth onClose={() => this.handleClose()}
|
||||
<DialogEx tag="UploadPresetDialog" open={this.props.open} fullWidth onClose={() => this.handleClose()}
|
||||
fullScreen={this.state.fullScreen}
|
||||
style={{userSelect: "none"}}
|
||||
>
|
||||
Reference in New Issue
Block a user