Plugin Presets.

This commit is contained in:
Robin Davies
2022-03-02 13:26:59 -05:00
parent f677b8b608
commit 2869ec0e00
32 changed files with 2401 additions and 914 deletions
+1 -1
View File
@@ -801,7 +801,7 @@ const AppThemed = withStyles(appStyles)(class extends ResizeResponsiveComponent<
<div className={classes.errorContentMask} />
<div style={{ flex: "2 2 3px", height: 20 }} >&nbsp;</div>
<div className={classes.errorMessageBox} style={{ position: "relative" }} >
<div style={{ fontSize: "30px", position: "absolute", left: 0, top: 0, color: "#A00" }}>
<div style={{ fontSize: "30px", position: "absolute", left: 0, top: 3, color: "#A00" }}>
<ErrorOutlineIcon color="inherit" fontSize="inherit" style={{ float: "left", marginRight: "12px" }} />
</div>
<div style={{ marginLeft: 40, marginTop: 3 }}>
+1 -1
View File
@@ -32,7 +32,7 @@ import ToobCabSimViewFactory from './ToobCabSimView';
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[] = [
+14 -6
View File
@@ -41,6 +41,7 @@ const styles = (theme: Theme) => createStyles({
interface GxTunerProps extends WithStyles<typeof styles> {
instanceId: number;
item: PedalBoardItem;
isToobTuner: boolean;
}
interface GxTunerState {
@@ -79,6 +80,11 @@ const GxTunerView =
{
let refFreqIndex = this.getControlIndex("REFFREQ");
let thresholdIndex = this.getControlIndex("THRESHOLD");
let muteIndex = -1;
if (this.props.isToobTuner)
{
muteIndex = this.getControlIndex("MUTE")
}
let result: (React.ReactNode | ControlGroup)[] = [];
@@ -87,6 +93,10 @@ const GxTunerView =
result.push(controls[refFreqIndex]);
result.push(controls[thresholdIndex]);
if (muteIndex !== -1)
{
result.push(controls[muteIndex]);
}
return result;
}
render() {
@@ -102,26 +112,24 @@ const GxTunerView =
class GxTunerViewFactory implements IControlViewFactory {
export class GxTunerViewFactory implements IControlViewFactory {
uri: string = GXTUNER_URI;
Create(model: PiPedalModel, pedalBoardItem: PedalBoardItem): React.ReactNode {
return (<GxTunerView instanceId={pedalBoardItem.instanceId} item={pedalBoardItem} />);
return (<GxTunerView isToobTuner={false} instanceId={pedalBoardItem.instanceId} item={pedalBoardItem} />);
}
}
// ToobTuner uses the same controls and control semantics.
class ToobTunerViewFactory implements IControlViewFactory {
export class ToobTunerViewFactory implements IControlViewFactory {
uri: string = TOOBTUNER_URI;
Create(model: PiPedalModel, pedalBoardItem: PedalBoardItem): React.ReactNode {
return (<GxTunerView instanceId={pedalBoardItem.instanceId} item={pedalBoardItem} />);
return (<GxTunerView isToobTuner={true} instanceId={pedalBoardItem.instanceId} item={pedalBoardItem} />);
}
}
export ToobTunerViewFactory;
export GxTunerViewFactory;
+6 -10
View File
@@ -55,8 +55,6 @@ const NARROW_DISPLAY_THRESHOLD = 600;
const FILTER_STORAGE_KEY = "com.twoplay.pipedal.load_dlg.filter";
let lastClickTime: number = 0;
const pluginGridStyles = (theme: Theme) => createStyles({
frame: {
@@ -327,7 +325,8 @@ export const LoadPluginDialog =
}
onDoubleClick(e: SyntheticEvent, uri: string): void {
this.props.onOk(uri);
// handled with onClick e.detail which provides better behaviour on Chrome.
// this.props.onOk(uri);
}
@@ -342,18 +341,15 @@ export const LoadPluginDialog =
}
onClick(e: SyntheticEvent, uri: string): void {
onClick(e: React.MouseEvent<HTMLDivElement>, uri: string): void {
e.preventDefault();
let currentTime = Date.now();
let dt = currentTime-lastClickTime;
let DOUBLE_CLICK_TIME = 500;
let isDoubleClick = dt < DOUBLE_CLICK_TIME;
lastClickTime = currentTime;
this.setState({ selected_uri: uri });
// a better doubleclick: tracks how many recent clicks in the same area.
// regardless of whether we re-rendered in the interrim.
let isDoubleClick = e.detail === 2;
// we have to synthesize double clicks because
// DOM rewrites interfere with natural double click.
if (isDoubleClick)
+4 -5
View File
@@ -184,8 +184,8 @@ export const MainPage =
}
}
handleSelectPluginPreset(instanceId: number, presetName: string) {
this.model.loadPluginPreset(instanceId, presetName);
handleSelectPluginPreset(instanceId: number, presetInstanceId: number) {
this.model.loadPluginPreset(instanceId, presetInstanceId);
}
onPedalBoardChanged(value: PedalBoard) {
this.setState({ pedalBoard: value });
@@ -325,12 +325,11 @@ export const MainPage =
<span className={classes.author}>{author}</span>
</span>
</div>
<div style={{ flex: "0 0 auto" }}>
<div style={{ flex: "0 0 auto", verticalAlign: "center" }}>
<PluginInfoDialog plugin_uri={pluginUri} />
</div>
<div style={{ flex: "0 0 auto" }}>
<PluginPresetSelector pluginUri={presetsUri}
onSelectPreset={(presetName) => this.handleSelectPluginPreset(pedalBoardItem!.instanceId, presetName)}
<PluginPresetSelector pluginUri={presetsUri} instanceId={pedalBoardItem?.instanceId ?? 0}
/>
</div>
</div>
+1 -1
View File
@@ -133,7 +133,7 @@ export class PedalBoardItem implements Deserializable<PedalBoardItem> {
return true;
}
}
throw new PiPedalArgumentError("Control not found.");
return false;
}
setMidiBinding(midiBinding: MidiBinding): boolean {
if (this.midiBindings)
+106 -16
View File
@@ -31,7 +31,7 @@ import { BankIndex } from './Banks';
import JackHostStatus from './JackHostStatus';
import JackServerSettings from './JackServerSettings';
import MidiBinding from './MidiBinding';
import PluginPreset from './PluginPreset';
import {PluginUiPresets} from './PluginPreset';
import WifiConfigSettings from './WifiConfigSettings';
import WifiChannel from './WifiChannel';
import AlsaDeviceInfo from './AlsaDeviceInfo';
@@ -47,6 +47,16 @@ export enum State {
export type ControlValueChangedHandler = (key: string, value: number) => void;
export type PluginPresetsChangedHandler = (pluginUri: string) => void;
export interface PluginPresetsChangedHandle {
_id: number;
_handler: PluginPresetsChangedHandler;
};
export interface ZoomedControlInfo {
source: HTMLElement;
instanceId: number;
@@ -72,7 +82,6 @@ export interface ControlValueChangedHandle {
_ControlValueChangedHandle: number;
};
interface ControlValueChangeItem {
handle: number;
instanceId: number;
@@ -341,6 +350,8 @@ export interface PiPedalModel {
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>;
@@ -372,8 +383,10 @@ export interface PiPedalModel {
getJackStatus(): Promise<JackHostStatus>;
getPluginPresets(uri: string): Promise<PluginPreset[]>;
loadPluginPreset(instanceId: number, presetName: string): void;
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>;
shutdown(): Promise<void>;
restart(): Promise<void>;
@@ -384,14 +397,17 @@ export interface PiPedalModel {
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): void;
download(targetType: string, isntanceId: number|string): void;
uploadPreset(file: File, uploadAfter: number): Promise<number>;
uploadPreset(uploadPage: string,file: File, uploadAfter: number): Promise<number>;
uploadBank(file: File, uploadAfter: number): Promise<number>;
setWifiConfigSettings(wifiConfigSettings: WifiConfigSettings): Promise<void>;
@@ -500,6 +516,9 @@ class PiPedalModelImpl implements PiPedalModel {
let presetsChangedBody = body as PresetsChangedBody;
let presets = new PresetIndex().deserialize(presetsChangedBody.presets);
this.presets.set(presets);
} else if (message === "onPluginPresetsChanged") {
let pluginUri = body as string;
this.handlePluginPresetsChanged(pluginUri);
} else if (message === "onJackConfigurationChanged") {
this.jackConfiguration.set(new JackConfiguration().deserialize(body));
} else if (message === "onLoadPluginPreset") {
@@ -973,6 +992,41 @@ class PiPedalModelImpl implements PiPedalModel {
}
}
_pluginPresetsChangedHandles: PluginPresetsChangedHandle[] = [];
addPluginPresetsChangedListener(onPluginPresetsChanged: PluginPresetsChangedHandler) : PluginPresetsChangedHandle {
let handle = ++this.nextListenHandle;
let t: PluginPresetsChangedHandle = {
_id: handle,
_handler: onPluginPresetsChanged
};
this._pluginPresetsChangedHandles.push(t);
return t;
}
removePluginPresetsChangedListener(handle: PluginPresetsChangedHandle): void
{
let pos = -1;
for (let i = 0; i < this._pluginPresetsChangedHandles.length; ++i)
{
if (this._pluginPresetsChangedHandles[i]._id === handle._id)
{
pos = i;
break;
}
}
if (pos !== -1)
{
this._pluginPresetsChangedHandles.splice(pos,1);
}
}
firePluginPresetsChanged(pluginUri: string): void {
for (let i = 0; i < this._pluginPresetsChangedHandles.length; ++i)
{
this._pluginPresetsChangedHandles[i]._handler(pluginUri);
}
}
_setPedalBoardControlValue(instanceId: number, key: string, value: number, notifyServer: boolean): void {
let pedalBoard = this.pedalBoard.get();
@@ -1244,6 +1298,18 @@ class PiPedalModelImpl implements PiPedalModel {
return data;
});
}
saveCurrentPluginPresetAs(pluginInstanceId: number,newName: string): Promise<number> {
// default behaviour is to save after the currently selected preset.
let request: any = {
instanceId: pluginInstanceId,
name: newName,
};
return nullCast(this.webSocket)
.request<number>("savePluginPresetAs", request)
.then((data) => {
return data;
});
}
loadPreset(instanceId: number): void {
@@ -1473,18 +1539,22 @@ class PiPedalModelImpl implements PiPedalModel {
return result;
}
presetCache: { [uri: string]: PluginPreset[]; } = {};
presetCache: { [uri: string]: PluginUiPresets } = {};
getPluginPresets(uri: string): Promise<PluginPreset[]> {
return new Promise<PluginPreset[]>((resolve, reject) => {
uncachePluginPreset(pluginUri: string): void {
delete this.presetCache[pluginUri];
}
getPluginPresets(uri: string): Promise<PluginUiPresets> {
return new Promise<PluginUiPresets>((resolve, reject) => {
if (this.presetCache[uri] !== undefined) {
resolve(this.presetCache[uri]);
} else {
this.webSocket!.request<any>("getPluginPresets", uri)
.then((result) => {
this.presetCache[uri] = PluginPreset.deserialize_array(result);
resolve(result);
let presets = new PluginUiPresets().deserialize(result);
this.presetCache[uri] = presets;
resolve(presets);
})
.catch((error) => {
reject(error);
@@ -1493,10 +1563,30 @@ class PiPedalModelImpl implements PiPedalModel {
});
}
loadPluginPreset(instanceId: number, presetName: string): void {
this.webSocket?.send("loadPluginPreset", { instanceId: instanceId, presetName: presetName });
loadPluginPreset(pluginInstanceId: number, presetInstanceId: number): void {
this.webSocket?.send("loadPluginPreset", { pluginInstanceId: pluginInstanceId, presetInstanceId: presetInstanceId });
}
handlePluginPresetsChanged(pluginUri: string):void {
this.uncachePluginPreset(pluginUri);
this.firePluginPresetsChanged(pluginUri);
}
updatePluginPresets(pluginUri: string, presets: PluginUiPresets): Promise<void>
{
this.presetCache[pluginUri] = presets;
this.firePluginPresetsChanged(pluginUri);
return nullCast(this.webSocket).request<void>("updatePluginPresets", presets);
}
duplicatePluginPreset(pluginUri: string, instanceId: number): Promise<number>
{
return nullCast(this.webSocket).request<number>("copyPluginPreset", {
pluginUri: pluginUri, instanceId: instanceId });
}
shutdown(): Promise<void> {
return this.webSocket!.request<void>("shutdown");
}
@@ -1596,19 +1686,19 @@ class PiPedalModelImpl implements PiPedalModel {
this.webSocket?.send("cancelListenForAtomOutput", listenHandle._handle);
}
download(targetType: string, instanceId: number): void {
download(targetType: string, instanceId: number | string): void {
if (instanceId === -1) return;
let url = this.varServerUrl + targetType + "?id=" + instanceId;
window.open(url, "_blank");
}
uploadPreset(file: File, uploadAfter: number): Promise<number> {
uploadPreset(uploadPage: string,file: File, uploadAfter: number): Promise<number> {
let result = new Promise<number>((resolve, reject) => {
try {
if (file.size > this.maxUploadSize) {
reject("File is too large.");
}
let url = this.varServerUrl + "uploadPreset";
let url = this.varServerUrl + uploadPage;
if (uploadAfter && uploadAfter !== -1) {
url += "?uploadAfter=" + uploadAfter;
}
+1 -1
View File
@@ -179,7 +179,7 @@ const PluginInfoDialog = withStyles(styles)((props: PluginInfoProps) => {
return (
<div>
<IconButton
style={{ display: (props.plugin_uri !== "") ? "block" : "none" }}
style={{ display: (props.plugin_uri !== "") ? "inline-flex" : "none" }}
onClick={handleClickOpen}
size="large">
<InfoOutlinedIcon />
+57 -9
View File
@@ -19,21 +19,69 @@
export default class PluginPreset {
deserialize(input: any): PluginPreset {
this.presetUri = input.presetUri;
this.name = input.name;
export class PluginUiPreset {
deserialize(input: any): PluginUiPreset {
this.instanceId = input.instanceId;
this.label = input.label;
return this;
}
static deserialize_array(input: any) : PluginPreset[] {
let result: PluginPreset[] = [];
static deserialize_array(input: any) : PluginUiPreset[] {
let result: PluginUiPreset[] = [];
for (let i = 0; i < input.length; ++i)
{
result.push(new PluginPreset().deserialize(input[i]));
result.push(new PluginUiPreset().deserialize(input[i]));
}
return result;
}
presetUri: string = "";
name: string = "";
instanceId: number = -1;
label: string = "";
static equals(left: PluginUiPreset, right: PluginUiPreset): boolean
{
return left.instanceId === right.instanceId && left.label === right.label;
}
}
export class PluginUiPresets {
deserialize(input: any): PluginUiPresets {
this.pluginUri = input.pluginUri;
this.presets = PluginUiPreset.deserialize_array(input.presets);
return this;
}
clone(): PluginUiPresets {
return new PluginUiPresets().deserialize(this);
}
movePreset(from: number, to: number): void
{
let t = this.presets[from];
this.presets.splice(from,1);
this.presets.splice(to,0,t);
}
getItem(instanceId: number): PluginUiPreset | undefined {
for (let i = 0; i < this.presets.length; ++i)
{
if (this.presets[i].instanceId === instanceId)
{
return this.presets[i];
}
}
return undefined;
}
static equals(left: PluginUiPresets,right: PluginUiPresets): boolean
{
if (left.pluginUri !== right.pluginUri) return false;
if (left.presets.length !== right.presets.length) return false;
for (let i = 0; i < left.presets.length; ++i)
{
if (!PluginUiPreset.equals(left.presets[i],right.presets[i]))
{
return false;
}
}
return true;
}
pluginUri: string = "";
presets: PluginUiPreset[] = [];
}
+91 -76
View File
@@ -19,30 +19,31 @@
import { SyntheticEvent, Component } from 'react';
import IconButton from '@mui/material/IconButton';
import Typography from '@mui/material/Typography';
import { PiPedalModel, PiPedalModelFactory } from './PiPedalModel';
import { PiPedalModel, PiPedalModelFactory, PluginPresetsChangedHandle } from './PiPedalModel';
import { Theme } from '@mui/material/styles';
import { WithStyles } from '@mui/styles';
import withStyles from '@mui/styles/withStyles';
import createStyles from '@mui/styles/createStyles';
import PresetDialog from './PresetDialog';
import PluginPresetsDialog from './PluginPresetsDialog';
import Menu from '@mui/material/Menu';
import MenuItem from '@mui/material/MenuItem';
import Fade from '@mui/material/Fade';
import RenameDialog from './RenameDialog'
import PluginPreset from './PluginPreset';
import {PluginUiPresets} from './PluginPreset';
import Divider from "@mui/material/Divider";
interface PluginPresetSelectorProps extends WithStyles<typeof styles> {
pluginUri?: string;
onSelectPreset: (presetName: string) => void;
instanceId: number;
}
interface PluginPresetSelectorState {
presets: PluginPreset[] | null;
presets : PluginUiPresets;
enabled: boolean;
presetChanged: boolean;
showPresetsDialog: boolean;
showEditPresetsDialog: boolean;
presetsMenuAnchorRef: HTMLElement | null;
@@ -52,10 +53,15 @@ interface PluginPresetSelectorState {
renameDialogActionName: string;
renameDialogOnOk?: (name: string) => void;
saveAsName: string;
}
const styles = (theme: Theme) => createStyles({
itemIcon: {
width: 24, height: 24, marginRight: "4px", opacity: 0.6
}
});
@@ -69,25 +75,27 @@ const PluginPresetSelector =
super(props);
this.model = PiPedalModelFactory.getInstance();
this.state = {
presets: null,
presets: new PluginUiPresets(),
enabled: false,
presetChanged: false,
showPresetsDialog: false,
showEditPresetsDialog: false,
presetsMenuAnchorRef: null,
renameDialogOpen: false,
renameDialogDefaultName: "",
renameDialogActionName: "",
renameDialogOnOk: undefined
renameDialogOnOk: undefined,
saveAsName: ""
};
this.handleDialogClose = this.handleDialogClose.bind(this);
this.handlePresetsMenuClose = this.handlePresetsMenuClose.bind(this);
}
handleLoadPluginPreset(presetName: string)
handleLoadPluginPreset(instanceId: number)
{
this.handlePresetsMenuClose();
this.props.onSelectPreset(presetName);
this.model.loadPluginPreset(this.props.instanceId,instanceId);
}
handlePresetMenuClick(e: SyntheticEvent): void {
@@ -97,24 +105,16 @@ const PluginPresetSelector =
this.setState({ presetsMenuAnchorRef: null });
}
handlePresetsMenuSave(e: SyntheticEvent): void {
handlePluginPresetsMenuSaveAs(e: SyntheticEvent): void {
this.handlePresetsMenuClose();
e.stopPropagation();
this.model.saveCurrentPreset();
}
handlePresetsMenuSaveAs(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;
let name = this.state.saveAsName;
this.renameDialogOpen(name, "Save As")
.then((newName) => {
return this.model.saveCurrentPresetAs(newName);
this.setState({saveAsName: newName});
return this.model.saveCurrentPluginPresetAs(this.props.instanceId,newName);
})
.then((newInstanceId) => {
// s'fine. dealt with by updates, but we do need error handling.
@@ -125,26 +125,6 @@ const PluginPresetSelector =
;
}
handlePresetsMenuRename(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.renameDialogOpen(name, "Rename")
.then((newName) => {
if (newName === name) return;
return this.model.renamePresetItem(this.model.presets.get().selectedInstanceId, newName);
})
.catch((error) => {
this.showError(error);
})
;
}
showError(error: string) {
this.model.showAlert(error);
}
@@ -168,6 +148,49 @@ const PluginPresetSelector =
return result;
}
loadPresets():void {
if (!this.currentUri) return;
let captureUri: string = this.currentUri;
this.model.getPluginPresets(captureUri)
.then((presets: PluginUiPresets) => {
if (captureUri === this.currentUri)
{
this.setState({presets: presets});
}
})
.catch(error => {
if (captureUri === this.currentUri)
{
this.setState({presets: new PluginUiPresets()});
}
});
}
onPluginPresetsChanged(pluginUri: string): void {
if (pluginUri === this.props.pluginUri)
{
this.loadPresets();
}
}
_pluginPresetsChangedHandle?: PluginPresetsChangedHandle;
componentDidMount()
{
this._pluginPresetsChangedHandle = this.model.addPluginPresetsChangedListener(
(pluginUri: string) => {
this.onPluginPresetsChanged(pluginUri);
}
);
}
componentWillUnmount()
{
if (this._pluginPresetsChangedHandle)
{
this.model.removePluginPresetsChangedListener(this._pluginPresetsChangedHandle);
this._pluginPresetsChangedHandle = undefined;
}
}
currentUri?: string = "";
componentDidUpdate()
{
@@ -176,23 +199,9 @@ const PluginPresetSelector =
this.currentUri = this.props.pluginUri;
if (!this.props.pluginUri)
{
this.setState({presets: null});
this.setState({presets: new PluginUiPresets()});
} else {
this.setState({presets: null});
let captureUri = this.currentUri;
this.model.getPluginPresets(this.props.pluginUri)
.then((presets: PluginPreset[]) => {
if (captureUri === this.currentUri)
{
this.setState({presets: presets});
}
})
.catch(error => {
if (captureUri === this.currentUri)
{
this.setState({presets: null});
}
});
this.loadPresets();
}
}
}
@@ -214,9 +223,9 @@ const PluginPresetSelector =
}
handleMenuEditPresets(): void {
handleMenuEditPluginPresets(): void {
this.handlePresetsMenuClose();
this.showEditPresetsDialog(true);
this.showEditPluginPresetsDialog(true);
}
handleSave() {
@@ -229,7 +238,7 @@ const PluginPresetSelector =
showEditPresetsDialog: false
});
}
showEditPresetsDialog(show: boolean) {
showEditPluginPresetsDialog(show: boolean) {
this.setState({
showPresetsDialog: show,
showEditPresetsDialog: true
@@ -252,9 +261,11 @@ const PluginPresetSelector =
}
render() {
let classes = this.props.classes;
//let classes = this.props.classes;
//let classes = this.props.classes;
if ((!this.props.pluginUri) || (!this.state.presets) ||this.state.presets.length === 0)
if ((!this.props.pluginUri))
{
return (<div/>);
}
@@ -276,23 +287,27 @@ const PluginPresetSelector =
}
>
<Typography variant="caption" color="textSecondary" style={{marginLeft: 16,marginTop:4,marginBottom: 4}}>Plugin presets</Typography>
<MenuItem onClick={(e) => this.handlePluginPresetsMenuSaveAs(e)}>Save plugin preset...</MenuItem>
<MenuItem onClick={(e) => this.handleMenuEditPluginPresets()}>Manage plugin presets...</MenuItem>
{ this.state.presets.presets.length !== 0 &&
(<Divider/>)
}
{
this.state.presets.map((preset) => {
return (<MenuItem key={preset.presetUri}
onClick={(e) => this.handleLoadPluginPreset(preset.presetUri)}>{preset.name}</MenuItem>);
this.state.presets.presets.map((preset) => {
return (<MenuItem key={preset.instanceId}
onClick={(e) => this.handleLoadPluginPreset(preset.instanceId)}
><img src="img/ic_pluginpreset2.svg" className={classes.itemIcon} alt="" />
{preset.label}</MenuItem>);
})
}
{/*
<Divider />
<MenuItem onClick={(e) => this.handlePresetsMenuSave(e)}>Save plugin preset</MenuItem>
<MenuItem onClick={(e) => this.handlePresetsMenuSaveAs(e)}>Save plugin preset as...</MenuItem>
<MenuItem onClick={(e) => this.handlePresetsMenuRename(e)}>Rename...</MenuItem>
<MenuItem onClick={(e) => this.handleMenuEditPresets()}>Manage presets...</MenuItem>
*/}
</Menu>
<PresetDialog show={this.state.showPresetsDialog} isEditDialog={this.state.showEditPresetsDialog} onDialogClose={() => this.handleDialogClose()} />
<PluginPresetsDialog
instanceId={this.props.instanceId}
presets={this.state.presets}
show={this.state.showPresetsDialog}
isEditDialog={this.state.showEditPresetsDialog}
onDialogClose={() => this.handleDialogClose()} />
<RenameDialog open={this.state.renameDialogOpen}
defaultName={this.state.renameDialogDefaultName}
acceptActionName={this.state.renameDialogActionName}
+488
View File
@@ -0,0 +1,488 @@
// Copyright (c) 2021 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, { SyntheticEvent,Component } from 'react';
import IconButton from '@mui/material/IconButton';
import Typography from '@mui/material/Typography';
import { PiPedalModel, PiPedalModelFactory } from './PiPedalModel';
import Button from "@mui/material/Button";
import ButtonBase from "@mui/material/ButtonBase";
import DialogEx from './DialogEx';
import AppBar from '@mui/material/AppBar';
import Toolbar from '@mui/material/Toolbar';
import DraggableGrid, { ScrollDirection } from './DraggableGrid';
import MoreVertIcon from '@mui/icons-material//MoreVert';
import ListItemIcon from '@mui/material/ListItemIcon';
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 {PluginUiPresets,PluginUiPreset} from './PluginPreset';
import SelectHoverBackground from './SelectHoverBackground';
import CloseIcon from '@mui/icons-material//Close';
import ArrowBackIcon from '@mui/icons-material//ArrowBack';
import EditIcon from '@mui/icons-material//Edit';
import RenameDialog from './RenameDialog';
import Slide, {SlideProps} from '@mui/material/Slide';
import { createStyles, Theme } from '@mui/material/styles';
import { WithStyles, withStyles} from '@mui/styles';
interface PluginPresetsDialogProps extends WithStyles<typeof styles> {
show: boolean;
isEditDialog: boolean;
instanceId: number;
presets :PluginUiPresets;
onDialogClose: () => void;
};
interface PluginPresetsDialogState {
showActionBar: boolean;
selectedItem: number;
renameOpen: boolean;
moreMenuAnchorEl: HTMLElement | null;
openUploadDialog: boolean;
};
const styles = (theme: Theme) => createStyles({
dialogAppBar: {
position: 'relative',
top: 0, left: 0
},
dialogActionBar: {
position: 'relative',
top: 0, left: 0,
background: "black"
},
dialogTitle: {
marginLeft: theme.spacing(2),
flex: 1,
},
itemFrame: {
display: "flex",
flexDirection: "row",
flexWrap: "nowrap",
width: "100%",
height: "56px",
alignItems: "center",
textAlign: "left",
justifyContent: "center",
paddingLeft: 8
},
iconFrame: {
flex: "0 0 auto",
},
itemIcon: {
width: 24, height: 24, margin: 12, opacity: 0.6
},
itemLabel: {
flex: "1 1 1px",
marginLeft: 8
}
});
const Transition = React.forwardRef(function Transition(
props: SlideProps, ref: React.Ref<unknown>
) {
return (<Slide direction="up" ref={ref} {...props} />);
});
const PluginPresetsDialog = withStyles(styles, { withTheme: true })(
class extends Component<PluginPresetsDialogProps, PluginPresetsDialogState> {
model: PiPedalModel;
constructor(props: PluginPresetsDialogProps) {
super(props);
this.model = PiPedalModelFactory.getInstance();
this.handleDialogClose = this.handleDialogClose.bind(this);
this.state = {
showActionBar: false,
selectedItem: -1,
renameOpen: false,
moreMenuAnchorEl: null,
openUploadDialog: false
};
}
selectItemAtIndex(index: number) {
let instanceId = this.props.presets.presets[index].instanceId;
this.setState({ selectedItem: instanceId });
}
isEditMode() {
return this.state.showActionBar || this.props.isEditDialog;
}
onMoreClick(e: SyntheticEvent): void {
this.setState({ moreMenuAnchorEl: e.currentTarget as HTMLElement })
}
handleDownloadPresets() {
this.handleMoreClose();
if (this.props.presets.pluginUri !== "")
{
this.model.download("downloadPluginPresets", this.props.presets.pluginUri);
}
}
handleUploadPresets() {
this.handleMoreClose();
this.setState({ openUploadDialog: true });
}
handleMoreClose(): void {
this.setState({ moreMenuAnchorEl: null });
}
componentDidUpdate()
{
if (!this.props.presets.getItem(this.state.selectedItem))
{
if (this.props.presets.presets.length !== 0)
{
this.setState({selectedItem: this.props.presets.presets[0].instanceId});
}
}
}
componentDidMount() {
// scroll selected item into view.
}
componentWillUnmount() {
}
getSelectedIndex() {
let instanceId = this.state.selectedItem;
let presets = this.props.presets;
for (let i = 0; i < presets.presets.length; ++i) {
if (presets.presets[i].instanceId === instanceId) return i;
}
return -1;
}
handleDeleteClick() {
let selectedItem = this.state.selectedItem;
if (selectedItem !== -1) {
let newPresets = this.props.presets.clone();
for (let i = 0; i < newPresets.presets.length; ++i)
{
if (newPresets.presets[i].instanceId === selectedItem)
{
newPresets.presets.splice(i,1);
this.model.updatePluginPresets(newPresets.pluginUri,newPresets)
.catch((error) => {
this.model.showAlert(error);
});
let newPos = i;
if (newPos >= newPresets.presets.length) {
--i;
}
let newSelection = i < 0? -1: newPresets.presets[i].instanceId;
this.setState({selectedItem: newSelection});
}
}
}
}
handleDialogClose() {
this.props.onDialogClose();
}
handleItemClick(instanceId: number): void {
if (this.isEditMode()) {
this.setState({ selectedItem: instanceId });
} else {
this.model.loadPreset(instanceId);
this.props.onDialogClose();
}
}
showActionBar(show: boolean): void {
this.setState({ showActionBar: show });
}
mapElement(el: any): React.ReactNode {
let presetEntry = el as PluginUiPreset;
let classes = this.props.classes;
let selectedItem = this.state.selectedItem;
return (
<div key={presetEntry.instanceId} style={{ background: "white" }} >
<ButtonBase style={{ width: "100%", height: 48 }}
onClick={() => this.handleItemClick(presetEntry.instanceId)}
>
<SelectHoverBackground selected={presetEntry.instanceId === selectedItem} showHover={true} />
<div className={classes.itemFrame}>
<div className={classes.iconFrame}>
<img src="img/ic_pluginpreset2.svg" className={classes.itemIcon} alt="" />
</div>
<div className={classes.itemLabel}>
<Typography>
{presetEntry.label}
</Typography>
</div>
</div>
</ButtonBase>
</div>
);
}
updateServerPluginPresets(newPresets: PluginUiPresets) {
newPresets = newPresets.clone();
this.model.updatePluginPresets(this.getPluginUri(),newPresets)
.catch((error) => {
this.model.showAlert(error);
});
}
moveElement(from: number, to: number): void {
let newPresets: PluginUiPresets = this.props.presets.clone();
newPresets.movePreset(from, to);
this.setState({
selectedItem: newPresets.presets[to].instanceId
});
this.updateServerPluginPresets(newPresets);
}
getSelectedName(): string {
let item = this.props.presets.getItem(this.state.selectedItem);
if (item) return item.label;
return "";
}
handleRenameClick() {
let item = this.props.presets.getItem(this.state.selectedItem);
if (item) {
this.setState({ renameOpen: true });
}
}
handleRenameOk(text: string) {
let item = this.props.presets.getItem(this.state.selectedItem);
if (!item) return;
if (item.label !== text) {
let newPresets = this.props.presets.clone();
let newItem = newPresets.getItem(this.state.selectedItem);
if (!newItem) return;
newItem.label = text;
this.updateServerPluginPresets(newPresets);
}
this.setState({ renameOpen: false });
}
getPluginUri() : string {
let pedalBoardItem = this.model.pedalBoard.get().getItem(this.props.instanceId);
return pedalBoardItem.uri;
}
handleCopy() {
let item = this.props.presets.getItem(this.state.selectedItem);
if (!item) return;
this.model.duplicatePluginPreset(this.getPluginUri(),this.state.selectedItem)
.then((newId) => {
this.setState({ selectedItem: newId });
}).catch((error) => {
this.onError(error);
});
}
onError(error: string): void {
this.model?.showAlert(error);
}
render() {
let classes = this.props.classes;
let actionBarClass = this.props.isEditDialog ? classes.dialogAppBar : classes.dialogActionBar;
let defaultSelectedIndex = this.getSelectedIndex();
let title: string = "";
let pluginUri = this.getPluginUri();
let plugin = this.model.getUiPlugin(pluginUri);
if (plugin) {
title = "Presets - " + plugin.name;
}
return (
<DialogEx tag="PluginPresetsDialog" fullScreen open={this.props.show}
onClose={() => { this.handleDialogClose() }} TransitionComponent={Transition}
style={{userSelect: "none"}}>
<div style={{ display: "flex", flexDirection: "column", flexWrap: "nowrap", width: "100%", height: "100%", overflow: "hidden" }}>
<div style={{ flex: "0 0 auto" }}>
<AppBar className={classes.dialogAppBar} style={{ display: this.isEditMode() ? "none" : "block" }} >
<Toolbar>
<IconButton edge="start" color="inherit" onClick={this.handleDialogClose} aria-label="back"
disabled={this.isEditMode()}
>
<ArrowBackIcon />
</IconButton>
<Typography variant="h6" className={classes.dialogTitle}>
{title}
</Typography>
<IconButton color="inherit" onClick={(e) => this.showActionBar(true)} >
<EditIcon />
</IconButton>
</Toolbar>
</AppBar>
<AppBar className={actionBarClass} style={{ display: this.isEditMode() ? "block" : "none" }}
onClick={(e) => { e.stopPropagation(); e.preventDefault(); }}
>
<Toolbar>
{(!this.props.isEditDialog) ? (
<IconButton edge="start" color="inherit" onClick={(e) => this.showActionBar(false)} aria-label="close">
<CloseIcon />
</IconButton>
) : (
<IconButton edge="start" color="inherit" onClick={this.handleDialogClose} aria-label="back"
>
<ArrowBackIcon />
</IconButton>
)}
<Typography variant="h6" className={classes.dialogTitle}>
{ title }
</Typography>
{(this.props.presets.getItem(this.state.selectedItem) != null)
&& (
<div style={{ flex: "0 0 auto" }}>
<Button color="inherit" onClick={(e) => this.handleCopy()}>
Copy
</Button>
<Button color="inherit" onClick={() => this.handleRenameClick()}>
Rename
</Button>
<RenameDialog
open={this.state.renameOpen}
defaultName={this.getSelectedName()}
acceptActionName={"Rename"}
onClose={() => { this.setState({ renameOpen: false }) }}
onOk={(text: string) => {
this.handleRenameOk(text);
}
}
/>
<IconButton color="inherit" onClick={(e) => this.handleDeleteClick()} >
<img src="/img/old_delete_outline_white_24dp.svg" alt="Delete" style={{ width: 24, height: 24, opacity: 0.6 }} />
</IconButton>
<IconButton color="inherit" onClick={(e) => { this.onMoreClick(e) }} >
<MoreVertIcon />
</IconButton>
<Menu
id="more-menu"
anchorEl={this.state.moreMenuAnchorEl}
keepMounted
open={Boolean(this.state.moreMenuAnchorEl)}
onClose={() => this.handleMoreClose()}
TransitionComponent={Fade}
>
<MenuItem onClick={() => { this.handleDownloadPresets(); }}
disabled={this.props.presets.presets.length === 0}
>
<ListItemIcon>
<img src="img/file_download_black_24dp.svg" style={{ width: 24, height: 24, opacity: 0.6 }} alt="" />
</ListItemIcon>
<ListItemText>
Download presets
</ListItemText>
</MenuItem>
<MenuItem onClick={() => { this.handleUploadPresets() }}>
<ListItemIcon>
<img src="img/file_upload_black_24dp.svg" style={{ width: 24, height: 24, opacity: 0.6 }} alt="" />
</ListItemIcon>
<ListItemText>
Upload presets
</ListItemText>
</MenuItem>
</Menu>
</div>
)
}
</Toolbar>
</AppBar>
</div>
<div style={{ flex: "1 1 auto", position: "relative", overflow: "hidden" }} >
<DraggableGrid
onLongPress={(item) => this.showActionBar(true)}
canDrag={this.isEditMode()}
onDragStart={(index, x, y) => { this.selectItemAtIndex(index) }}
moveElement={(from, to) => { this.moveElement(from, to); }}
scroll={ScrollDirection.Y}
defaultSelectedIndex={defaultSelectedIndex}
>
{
this.props.presets.presets.map((element) => {
return this.mapElement(element);
})
}
</DraggableGrid>
</div>
</div>
<UploadDialog
title="Upload Plugin Presets"
extension='.piPluginPresets'
uploadPage='uploadPluginPresets'
onUploaded={(instanceId) => {} }
uploadAfter={this.state.selectedItem}
open={this.state.openUploadDialog}
onClose={() => { this.setState({ openUploadDialog: false }) }} />
</DialogEx>
);
}
}
);
export default PluginPresetsDialog;
+3
View File
@@ -446,6 +446,9 @@ const PresetDialog = withStyles(styles, { withTheme: true })(
</div>
</div>
<UploadDialog
title='Upload preset'
extension='.piPreset'
uploadPage='uploadPreset'
onUploaded={(instanceId) => this.setState({selectedItem: instanceId}) }
uploadAfter={this.state.selectedItem}
open={this.state.openUploadDialog}
+5 -1
View File
@@ -369,7 +369,11 @@ const PresetSelector =
acceptActionName={this.state.renameDialogActionName}
onClose={() => this.handleRenameDialogClose()}
onOk={(name: string) => this.handleRenameDialogOk(name)} />
<UploadDialog onUploaded={(instanceId) => { this.model.loadPreset(instanceId); }}
<UploadDialog
title='Upload preset'
extension='.piPreset'
uploadPage='uploadPreset'
onUploaded={(instanceId) => { this.model.loadPreset(instanceId); }}
open={this.state.openUploadDialog}
uploadAfter={-1}
onClose={() => { this.setState({ openUploadDialog: false }) }} />
+2
View File
@@ -85,6 +85,8 @@ export default class RenameDialog extends ResizeResponsiveComponent<RenameDialog
const handleOk = () => {
let text = nullCast(this.refText.current).value;
text = text.trim();
if (text.length === 0) return;
onOk(text);
}
const handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>): void => {
+12 -8
View File
@@ -29,14 +29,18 @@ import ResizeResponsiveComponent from './ResizeResponsiveComponent';
import Typography from '@mui/material/Typography';
const PRESET_EXTENSION = ".piPreset";
const BANK_EXTENSION = ".piBank";
// const PRESET_EXTENSION = ".piPreset";
// const BANK_EXTENSION = ".piBank";
// const PLUGIN_PRESETS_EXTENSION = ".piPluginPresets";
export interface UploadDialogProps {
open: boolean,
onClose: () => void,
uploadAfter: number,
onUploaded: (instanceId: number) => void
onUploaded: (instanceId: number) => void,
title: string,
extension: string,
uploadPage: string
};
@@ -94,11 +98,11 @@ export default class UploadDialog extends ResizeResponsiveComponent<UploadDialog
let uploadAfter = this.props.uploadAfter;
for (let i = 0; i < fileList.length; ++i)
{
uploadAfter = await this.model.uploadPreset(fileList[i],uploadAfter);
uploadAfter = await this.model.uploadPreset(this.props.uploadPage,fileList[i],uploadAfter);
}
this.props.onUploaded(uploadAfter);
} catch(error) {
this.model.showAlert(error);
this.model.showAlert(error +"");
};
this.props.onClose();
}
@@ -132,7 +136,7 @@ export default class UploadDialog extends ResizeResponsiveComponent<UploadDialog
for (let i = 0; i < fileList.files.length; ++i) {
let file = fileList.files[i];
let extension = this.getFileExtension(file.name);
if (extension !== PRESET_EXTENSION && extension !== BANK_EXTENSION) return false;
if (extension !== this.props.extension) return false;
}
return true;
}
@@ -157,7 +161,7 @@ export default class UploadDialog extends ResizeResponsiveComponent<UploadDialog
fullScreen={this.state.fullScreen}
style={{userSelect: "none"}}
>
<DialogTitle>Upload preset</DialogTitle>
<DialogTitle>{this.props.title}</DialogTitle>
<DialogContent>
<div style={{
width: "100%", height: 100, marginBottom: 0,
@@ -188,7 +192,7 @@ export default class UploadDialog extends ResizeResponsiveComponent<UploadDialog
<input
type="file"
hidden
accept=".piPreset"
accept={this.props.extension}
multiple
onChange={(e)=> this.handleButtonSelect(e)}
/>