This commit is contained in:
Robin Davies
2023-04-16 01:13:03 -04:00
parent d05c7c7104
commit 845af2ee47
93 changed files with 3987 additions and 1688 deletions
+1 -1
View File
@@ -79,7 +79,7 @@ add_custom_command(
src/Banks.tsx
src/PluginPresetsDialog.tsx
src/BankDialog.tsx
src/UploadDialog.tsx
src/UploadPresetDialog.tsx
src/ToobPowerStage2View.tsx
src/index.tsx
src/Units.tsx
+3
View File
@@ -12,3 +12,6 @@
padding-left: 36px;
border-bottom: 1px #CCC solid
}
div {
min-width: 0;
}
-1
View File
@@ -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>
+16 -16
View File
@@ -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>
+7 -3
View File
@@ -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}
+155 -81
View File
@@ -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" }}>&nbsp;</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" }}>&nbsp;</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" }}>&nbsp;</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" }}>&nbsp;</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" }}>&nbsp;</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" }}>&nbsp;</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
View File
@@ -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
View File
@@ -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
View File
@@ -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) && (
+9
View File
@@ -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;
+42 -3
View File
@@ -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)
{
+81 -70
View File
@@ -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
View File
@@ -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 -1
View File
@@ -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
+71 -3
View File
@@ -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);
+252
View File
@@ -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 }}>
&nbsp;
</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 }}>
&nbsp;
</div>
</div >
);
}
}
}
);
export default PluginOutputControl;
+7 -7
View File
@@ -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>
+7 -7
View File
@@ -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>
+39 -19
View File
@@ -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>
);
+34 -24
View File
@@ -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>);
}
+414
View File
@@ -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"}}
>
+1 -1
View File
@@ -49,7 +49,7 @@ src/GxTunerControl.tsx
src/Banks.tsx
src/PluginPresetsDialog.tsx
src/BankDialog.tsx
src/UploadDialog.tsx
src/UploadPresetDialog.tsx
src/ToobPowerStage2View.tsx
src/index.tsx
src/Units.tsx