From 2869ec0e003ebdd7143021e4e8533bee3828d99d Mon Sep 17 00:00:00 2001 From: Robin Davies Date: Wed, 2 Mar 2022 13:26:59 -0500 Subject: [PATCH] Plugin Presets. --- react/public/img/ic_pluginpreset2.svg | 12 + react/public/var/config.json | 4 +- react/src/AppThemed.tsx | 2 +- react/src/ControlViewFactory.tsx | 2 +- react/src/GxTunerView.tsx | 20 +- react/src/LoadPluginDialog.tsx | 16 +- react/src/MainPage.tsx | 9 +- react/src/PedalBoard.tsx | 2 +- react/src/PiPedalModel.tsx | 122 +++- react/src/PluginInfoDialog.tsx | 2 +- react/src/PluginPreset.tsx | 66 +- react/src/PluginPresetSelector.tsx | 167 +++-- react/src/PluginPresetsDialog.tsx | 488 +++++++++++++ react/src/PresetDialog.tsx | 3 + react/src/PresetSelector.tsx | 6 +- react/src/RenameDialog.tsx | 2 + react/src/UploadDialog.tsx | 20 +- src/BeastServer.cpp | 996 ++++++++++++++------------ src/BeastServer.hpp | 3 +- src/CMakeLists.txt | 1 + src/Lv2Host.cpp | 99 ++- src/Lv2Host.hpp | 32 +- src/PiPedalModel.cpp | 336 +++++---- src/PiPedalModel.hpp | 119 +-- src/PiPedalSocket.cpp | 139 ++-- src/PluginPreset.cpp | 47 ++ src/PluginPreset.hpp | 136 ++++ src/Storage.cpp | 277 ++++++- src/Storage.hpp | 21 +- src/json.cpp | 6 +- src/json.hpp | 54 +- src/main.cpp | 106 ++- 32 files changed, 2401 insertions(+), 914 deletions(-) create mode 100755 react/public/img/ic_pluginpreset2.svg create mode 100644 react/src/PluginPresetsDialog.tsx create mode 100644 src/PluginPreset.cpp create mode 100644 src/PluginPreset.hpp diff --git a/react/public/img/ic_pluginpreset2.svg b/react/public/img/ic_pluginpreset2.svg new file mode 100755 index 0000000..49578ce --- /dev/null +++ b/react/public/img/ic_pluginpreset2.svg @@ -0,0 +1,12 @@ + + + + + + + + + diff --git a/react/public/var/config.json b/react/public/var/config.json index c1902a0..93f81dd 100644 --- a/react/public/var/config.json +++ b/react/public/var/config.json @@ -1,5 +1,5 @@ { - "socket_server_port": 80, + "socket_server_port": 8080, "socket_server_address": "*", "max_upload_size": 1048576, @@ -8,4 +8,4 @@ ] -} \ No newline at end of file +} \ No newline at end of file diff --git a/react/src/AppThemed.tsx b/react/src/AppThemed.tsx index a1c69bd..f2c16be 100644 --- a/react/src/AppThemed.tsx +++ b/react/src/AppThemed.tsx @@ -801,7 +801,7 @@ const AppThemed = withStyles(appStyles)(class extends ResizeResponsiveComponent<
 
-
+
diff --git a/react/src/ControlViewFactory.tsx b/react/src/ControlViewFactory.tsx index 8857168..673074b 100644 --- a/react/src/ControlViewFactory.tsx +++ b/react/src/ControlViewFactory.tsx @@ -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[] = [ diff --git a/react/src/GxTunerView.tsx b/react/src/GxTunerView.tsx index eee7f95..a0d7b91 100644 --- a/react/src/GxTunerView.tsx +++ b/react/src/GxTunerView.tsx @@ -41,6 +41,7 @@ const styles = (theme: Theme) => createStyles({ interface GxTunerProps extends WithStyles { 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 (); + return (); } } // 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 (); + return (); } } -export ToobTunerViewFactory; -export GxTunerViewFactory; \ No newline at end of file diff --git a/react/src/LoadPluginDialog.tsx b/react/src/LoadPluginDialog.tsx index bf619fd..b37e015 100644 --- a/react/src/LoadPluginDialog.tsx +++ b/react/src/LoadPluginDialog.tsx @@ -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, 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) diff --git a/react/src/MainPage.tsx b/react/src/MainPage.tsx index 4e946fd..337f3b0 100644 --- a/react/src/MainPage.tsx +++ b/react/src/MainPage.tsx @@ -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 = {author}
-
+
- this.handleSelectPluginPreset(pedalBoardItem!.instanceId, presetName)} +
diff --git a/react/src/PedalBoard.tsx b/react/src/PedalBoard.tsx index 5b35fd2..782cc60 100644 --- a/react/src/PedalBoard.tsx +++ b/react/src/PedalBoard.tsx @@ -133,7 +133,7 @@ export class PedalBoardItem implements Deserializable { return true; } } - throw new PiPedalArgumentError("Control not found."); + return false; } setMidiBinding(midiBinding: MidiBinding): boolean { if (this.midiBindings) diff --git a/react/src/PiPedalModel.tsx b/react/src/PiPedalModel.tsx index 75e1729..6e2af5d 100644 --- a/react/src/PiPedalModel.tsx +++ b/react/src/PiPedalModel.tsx @@ -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; deletePresetItem(instanceId: number): Promise; @@ -372,8 +383,10 @@ export interface PiPedalModel { getJackStatus(): Promise; - getPluginPresets(uri: string): Promise; - loadPluginPreset(instanceId: number, presetName: string): void; + getPluginPresets(uri: string): Promise; + loadPluginPreset(pluginInstanceId: number, presetInstanceId: number): void; + updatePluginPresets(uri: string, presets: PluginUiPresets): Promise; + duplicatePluginPreset(uri: string, instanceId: number): Promise; shutdown(): Promise; restart(): Promise; @@ -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; + uploadPreset(uploadPage: string,file: File, uploadAfter: number): Promise; uploadBank(file: File, uploadAfter: number): Promise; setWifiConfigSettings(wifiConfigSettings: WifiConfigSettings): Promise; @@ -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 { + // default behaviour is to save after the currently selected preset. + let request: any = { + instanceId: pluginInstanceId, + name: newName, + }; + return nullCast(this.webSocket) + .request("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 { - return new Promise((resolve, reject) => { + uncachePluginPreset(pluginUri: string): void { + delete this.presetCache[pluginUri]; + } + getPluginPresets(uri: string): Promise { + return new Promise((resolve, reject) => { if (this.presetCache[uri] !== undefined) { resolve(this.presetCache[uri]); } else { this.webSocket!.request("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 + { + this.presetCache[pluginUri] = presets; + this.firePluginPresetsChanged(pluginUri); + return nullCast(this.webSocket).request("updatePluginPresets", presets); + } + duplicatePluginPreset(pluginUri: string, instanceId: number): Promise + { + return nullCast(this.webSocket).request("copyPluginPreset", { + pluginUri: pluginUri, instanceId: instanceId }); + + } + + shutdown(): Promise { return this.webSocket!.request("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 { + uploadPreset(uploadPage: string,file: File, uploadAfter: number): Promise { let result = new Promise((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; } diff --git a/react/src/PluginInfoDialog.tsx b/react/src/PluginInfoDialog.tsx index e0c3f30..5f0d9a6 100644 --- a/react/src/PluginInfoDialog.tsx +++ b/react/src/PluginInfoDialog.tsx @@ -179,7 +179,7 @@ const PluginInfoDialog = withStyles(styles)((props: PluginInfoProps) => { return (
diff --git a/react/src/PluginPreset.tsx b/react/src/PluginPreset.tsx index 5a254c1..5b97869 100644 --- a/react/src/PluginPreset.tsx +++ b/react/src/PluginPreset.tsx @@ -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[] = []; } \ No newline at end of file diff --git a/react/src/PluginPresetSelector.tsx b/react/src/PluginPresetSelector.tsx index 7d5193d..6a4f064 100644 --- a/react/src/PluginPresetSelector.tsx +++ b/react/src/PluginPresetSelector.tsx @@ -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 { 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 (
); } @@ -276,23 +287,27 @@ const PluginPresetSelector = } > - Plugin presets + this.handlePluginPresetsMenuSaveAs(e)}>Save plugin preset... + this.handleMenuEditPluginPresets()}>Manage plugin presets... + + { this.state.presets.presets.length !== 0 && + () + } { - this.state.presets.map((preset) => { - return ( this.handleLoadPluginPreset(preset.presetUri)}>{preset.name}); + this.state.presets.presets.map((preset) => { + return ( this.handleLoadPluginPreset(preset.instanceId)} + > + {preset.label}); }) } - {/* - - this.handlePresetsMenuSave(e)}>Save plugin preset - this.handlePresetsMenuSaveAs(e)}>Save plugin preset as... - this.handlePresetsMenuRename(e)}>Rename... - this.handleMenuEditPresets()}>Manage presets... - */} - - this.handleDialogClose()} /> + this.handleDialogClose()} /> { + 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 +) { + return (); +}); + + +const PluginPresetsDialog = withStyles(styles, { withTheme: true })( + + class extends Component { + + 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 ( +
+ + this.handleItemClick(presetEntry.instanceId)} + > + +
+
+ +
+
+ + {presetEntry.label} + +
+
+
+
+ + ); + } + + 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 ( + { this.handleDialogClose() }} TransitionComponent={Transition} + style={{userSelect: "none"}}> +
+
+ + + + + + + {title} + + this.showActionBar(true)} > + + + + + { e.stopPropagation(); e.preventDefault(); }} + > + + {(!this.props.isEditDialog) ? ( + this.showActionBar(false)} aria-label="close"> + + + ) : ( + + + + + )} + + { title } + + {(this.props.presets.getItem(this.state.selectedItem) != null) + && ( + +
+ + + { this.setState({ renameOpen: false }) }} + onOk={(text: string) => { + this.handleRenameOk(text); + } + } + /> + this.handleDeleteClick()} > + Delete + + { this.onMoreClick(e) }} > + + + this.handleMoreClose()} + TransitionComponent={Fade} + > + { this.handleDownloadPresets(); }} + disabled={this.props.presets.presets.length === 0} + > + + + + + Download presets + + + + { this.handleUploadPresets() }}> + + + + + Upload presets + + + + + + +
+ ) + } +
+ +
+
+
+ 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); + }) + } + +
+
+ {} } + uploadAfter={this.state.selectedItem} + open={this.state.openUploadDialog} + onClose={() => { this.setState({ openUploadDialog: false }) }} /> + +
+ + ); + + } + + } + +); + +export default PluginPresetsDialog; \ No newline at end of file diff --git a/react/src/PresetDialog.tsx b/react/src/PresetDialog.tsx index a1093f3..9aca799 100644 --- a/react/src/PresetDialog.tsx +++ b/react/src/PresetDialog.tsx @@ -446,6 +446,9 @@ const PresetDialog = withStyles(styles, { withTheme: true })(
this.setState({selectedItem: instanceId}) } uploadAfter={this.state.selectedItem} open={this.state.openUploadDialog} diff --git a/react/src/PresetSelector.tsx b/react/src/PresetSelector.tsx index 96d9949..810aa3b 100644 --- a/react/src/PresetSelector.tsx +++ b/react/src/PresetSelector.tsx @@ -369,7 +369,11 @@ const PresetSelector = acceptActionName={this.state.renameDialogActionName} onClose={() => this.handleRenameDialogClose()} onOk={(name: string) => this.handleRenameDialogOk(name)} /> - { this.model.loadPreset(instanceId); }} + { this.model.loadPreset(instanceId); }} open={this.state.openUploadDialog} uploadAfter={-1} onClose={() => { this.setState({ openUploadDialog: false }) }} /> diff --git a/react/src/RenameDialog.tsx b/react/src/RenameDialog.tsx index 650a9d3..a37126c 100644 --- a/react/src/RenameDialog.tsx +++ b/react/src/RenameDialog.tsx @@ -85,6 +85,8 @@ export default class RenameDialog extends ResizeResponsiveComponent { let text = nullCast(this.refText.current).value; + text = text.trim(); + if (text.length === 0) return; onOk(text); } const handleKeyDown = (event: React.KeyboardEvent): void => { diff --git a/react/src/UploadDialog.tsx b/react/src/UploadDialog.tsx index b4fa016..ba3e69f 100644 --- a/react/src/UploadDialog.tsx +++ b/react/src/UploadDialog.tsx @@ -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 - Upload preset + {this.props.title}