Checkpoint
@@ -108,7 +108,6 @@ add_custom_command(
|
||||
public/iso_codes.json
|
||||
public/logo512.png
|
||||
public/sample_lv2_plugins.json
|
||||
public/vite.svg
|
||||
src/pipedal/SplitUiControls.tsx
|
||||
src/pipedal/JackHostStatus.tsx
|
||||
src/pipedal/VirtualKeyboardHandler.tsx
|
||||
@@ -291,8 +290,6 @@ add_custom_command(
|
||||
src/pipedal/MidiChannelBinding.tsx
|
||||
src/pipedal/ModFileTypes.tsx
|
||||
src/assets/react.svg
|
||||
src/App.css
|
||||
src/App.tsx
|
||||
src/vite-env.d.ts
|
||||
src/index.css
|
||||
src/main.tsx
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<link rel="stylesheet" href="/css/roboto.css" />
|
||||
<link rel="apple-touch-icon" href="/logo192.png" />
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
|
||||
<link rel="stylesheet" href="/css/modGui.css" />
|
||||
<style>
|
||||
BODY {
|
||||
background: #D0D0D0;
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
.mod-pedal {
|
||||
}
|
||||
|
||||
.mod-pedal .mod-light {
|
||||
}
|
||||
.mod-pedal .mod-light.off { background-image:url(../img/red-light-off.png); }
|
||||
.mod-pedal .mod-light.on { background-image:url(../img/red-light-on.png); }
|
||||
|
||||
.mod-pedal .mod-pedal-input {
|
||||
position: absolute;
|
||||
top: 70px;
|
||||
left: -24px;
|
||||
width: 24px;
|
||||
|
||||
}
|
||||
|
||||
.mod-pedal .mod-pedal-output {
|
||||
position: absolute;
|
||||
top: 70px;
|
||||
right: -25px;
|
||||
width: 25px;
|
||||
}
|
||||
|
||||
.mod-pedal .mod-pedal-input .mod-audio-input,
|
||||
.mod-pedal .mod-pedal-input .mod-midi-input,
|
||||
.mod-pedal .mod-pedal-input .mod-cv-input {
|
||||
position: relative;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.mod-audio-input .mod-pedal-input-image {
|
||||
background-image:url(../img/audio-input.png) ;
|
||||
width: 24px;
|
||||
height: 56px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.mod-audio-output .mod-pedal-output-image {
|
||||
background-image:url(../img/audio-output.png);
|
||||
width: 24px;
|
||||
height: 56px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.ppmod-dial-control-image {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
object-fit: fill;
|
||||
|
||||
}
|
||||
.ppmod-dial-control-frame
|
||||
{
|
||||
position: relative;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.mod-knob-title {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
.mod-slider-title {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@@ -2,28 +2,28 @@
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
font-display: swap;
|
||||
font-display: block;
|
||||
src: url(/fonts/Roboto-Light.woff2) format('woff2');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
font-display: block;
|
||||
src: url(/fonts/Roboto-Regular.woff2) format('woff2');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
font-display: swap;
|
||||
font-display: block;
|
||||
src: url(/fonts/Roboto-Medium.woff2) format('woff2');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
font-display: block;
|
||||
src: url(/fonts/Roboto-Bold.woff2) format('woff2');
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
|
||||
@font-face {
|
||||
font-family: 'EnglandHand';
|
||||
src: url('england-webfont.woff');
|
||||
font-weight: normal;
|
||||
font-display: swap;
|
||||
font-style: normal;
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
@font-face {
|
||||
font-family: 'epflul';
|
||||
src: url('epf_lul-webfont.woff') format('woff');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* Web Fonts from fontspring.com
|
||||
*
|
||||
* All OpenType features and all extended glyphs have been removed.
|
||||
* Fully installable fonts can be purchased at http://www.fontspring.com
|
||||
*
|
||||
* The fonts included in this stylesheet are subject to the End User License you purchased
|
||||
* from Fontspring. The fonts are protected under domestic and international trademark and
|
||||
* copyright law. You are prohibited from modifying, reverse engineering, duplicating, or
|
||||
* distributing this font software.
|
||||
*
|
||||
* (c) 2010-2012 Fontspring
|
||||
*
|
||||
*
|
||||
*
|
||||
*
|
||||
* The fonts included are copyrighted by the vendor listed below.
|
||||
*
|
||||
* Vendor: Fontfabric
|
||||
* License URL: http://www.fontspring.com/fflicense/fontfabric
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
@font-face {
|
||||
font-family: 'Nexa';
|
||||
src: url('Nexa_Free_Bold-webfont.woff') format('woff');
|
||||
font-display: block;
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
/* Generated by Font Squirrel (http://www.fontsquirrel.com) on February 25, 2013 */
|
||||
|
||||
@font-face {
|
||||
font-family: 'Pirulen';
|
||||
src: url('pirulen_rg-webfont.woff') format('woff');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
/* Generated by Font Squirrel (http://www.fontsquirrel.com) on February 27, 2013 */
|
||||
|
||||
@font-face {
|
||||
font-family: 'Questrial';
|
||||
src: url('questrial-regular-webfont.woff') format('woff');
|
||||
font-display: block;
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
|
||||
}
|
||||
|
After Width: | Height: | Size: 112 KiB |
|
After Width: | Height: | Size: 5.5 KiB |
|
After Width: | Height: | Size: 5.5 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 766 B |
|
After Width: | Height: | Size: 1.7 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||
|
Before Width: | Height: | Size: 1.5 KiB |
@@ -1,36 +0,0 @@
|
||||
import { useState } from 'react'
|
||||
import ReactLogo from './assets/react.svg?react'
|
||||
//import ViteLogo from '/vite.svg?react'
|
||||
import './App.css'
|
||||
|
||||
|
||||
function App() {
|
||||
const [count, setCount] = useState(0)
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<a href="https://vite.dev" target="_blank">
|
||||
{/* <ViteLogo/> */}
|
||||
</a>
|
||||
<a href="https://react.dev" target="_blank">
|
||||
<ReactLogo style={{background: "blue", padding: 96}} />
|
||||
</a>
|
||||
</div>
|
||||
<h1>Vite + React</h1>
|
||||
<div className="card">
|
||||
<button onClick={() => setCount((count) => count + 1)}>
|
||||
count is {count}
|
||||
</button>
|
||||
<p>
|
||||
Edit <code>src/App.tsx</code> and save to test HMR
|
||||
</p>
|
||||
</div>
|
||||
<p className="read-the-docs">
|
||||
Click on the Vite and React logos to learn more
|
||||
</p>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default App
|
||||
@@ -152,7 +152,7 @@ const AboutDialog = class extends Component<AboutDialogProps, AboutDialogState>
|
||||
<ArrowBackIcon />
|
||||
</IconButtonEx>
|
||||
<Typography noWrap variant="h6"
|
||||
sx={{ maringLeft: 2, flex: 1 }}
|
||||
sx={{ marginLeft: 2, flex: 1 }}
|
||||
>
|
||||
About
|
||||
</Typography>
|
||||
|
||||
@@ -18,13 +18,15 @@
|
||||
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { ThemeProvider, createTheme, StyledEngineProvider } from '@mui/material/styles';
|
||||
import CssBaseline from '@mui/material/CssBaseline';
|
||||
import CssBaseline from '@mui/material/CssBaseline';
|
||||
|
||||
import VirtualKeyboardHandler from './VirtualKeyboardHandler';
|
||||
import AppThemed from "./AppThemed";
|
||||
import { isDarkMode } from './DarkMode';
|
||||
import Tone3000AuthComplete from './Tone3000AuthComplete';
|
||||
import FontTest from './FontTest';
|
||||
|
||||
|
||||
declare module '@mui/material/styles' {
|
||||
@@ -225,8 +227,13 @@ type AppThemeProps = {
|
||||
|
||||
function isTone3000Auth() {
|
||||
let url = new URL(window.location.href);
|
||||
let param = url.searchParams.get("api_key");
|
||||
return (param !== null && param !== "")
|
||||
let param = url.searchParams.get("api_key");
|
||||
return (param !== null && param !== "")
|
||||
}
|
||||
function isFontTest() {
|
||||
let url = new URL(window.location.href);
|
||||
let param = url.searchParams.get("fontTest");
|
||||
return (param !== null)
|
||||
}
|
||||
|
||||
const App = (class extends React.Component {
|
||||
@@ -248,11 +255,11 @@ const App = (class extends React.Component {
|
||||
<StyledEngineProvider injectFirst>
|
||||
<ThemeProvider theme={theme}>
|
||||
<CssBaseline />
|
||||
{isTone3000Auth() ? (
|
||||
<Tone3000AuthComplete />
|
||||
) : (
|
||||
<AppThemed />
|
||||
)}
|
||||
{
|
||||
isTone3000Auth() && (<Tone3000AuthComplete />)
|
||||
|| isFontTest() && (<FontTest />)
|
||||
|| (<AppThemed />)
|
||||
}
|
||||
</ThemeProvider>
|
||||
</StyledEngineProvider>
|
||||
);
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
|
||||
|
||||
import './AppThemed.css';
|
||||
|
||||
//import {alpha} from '@mui/material/styles';
|
||||
import AppBar from '@mui/material/AppBar';
|
||||
|
||||
@@ -74,6 +75,7 @@ import HelpOutlineIcon from './svg/ic_help_outline.svg?react';
|
||||
import FxAmplifierIcon from './svg/fx_amplifier.svg?react';
|
||||
import { PerformanceView } from './PerformanceView';
|
||||
|
||||
|
||||
import DialogEx from './DialogEx';
|
||||
|
||||
import { IDialogStackable, popDialogStack, pushDialogStack } from './DialogStack';
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Robin E. R. Davies
|
||||
* All rights reserved.
|
||||
|
||||
* 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.
|
||||
*/
|
||||
|
||||
@@ -42,7 +42,11 @@ let pluginFactories: IControlViewFactory[] = [
|
||||
];
|
||||
|
||||
|
||||
export function GetControlView(pedalboardItem?: PedalboardItem | null): React.ReactNode {
|
||||
export function GetControlView(
|
||||
pedalboardItem: PedalboardItem | null,
|
||||
showModUi: boolean,
|
||||
onSetShowModGui: (instanceId: number, showModGui: boolean) => void
|
||||
): React.ReactNode {
|
||||
let model: PiPedalModel = PiPedalModelFactory.getInstance();
|
||||
|
||||
if (!pedalboardItem) {
|
||||
@@ -50,7 +54,13 @@ export function GetControlView(pedalboardItem?: PedalboardItem | null): React.Re
|
||||
}
|
||||
if (pedalboardItem.isStart() || pedalboardItem.isEnd()) {
|
||||
return (
|
||||
<PluginControlView instanceId={pedalboardItem.instanceId} item={pedalboardItem} />
|
||||
<PluginControlView instanceId={pedalboardItem.instanceId}
|
||||
item={pedalboardItem}
|
||||
showModGui={showModUi}
|
||||
onSetShowModGui={(instanceId, showModGui) => {
|
||||
onSetShowModGui?.(instanceId, showModGui);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (pedalboardItem.isSplit()) {
|
||||
@@ -73,7 +83,11 @@ export function GetControlView(pedalboardItem?: PedalboardItem | null): React.Re
|
||||
</div>
|
||||
} else {
|
||||
return (
|
||||
<PluginControlView instanceId={pedalboardItem.instanceId} item={pedalboardItem} />
|
||||
<PluginControlView instanceId={pedalboardItem.instanceId} item={pedalboardItem} showModGui={showModUi}
|
||||
onSetShowModGui={(instanceId, showModGui) => {
|
||||
onSetShowModGui?.(instanceId, showModGui);
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Robin E. R. Davies
|
||||
* All rights reserved.
|
||||
|
||||
* 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 Divider from '@mui/material/Divider';
|
||||
|
||||
function TypeSample(props: { fontFamily: string, cssRef: string, fontWeights: number[] }) {
|
||||
let { fontFamily, cssRef, fontWeights } = props;
|
||||
React.useEffect(() => {
|
||||
let link = document.createElement('link');
|
||||
link.rel = 'stylesheet';
|
||||
link.href = cssRef;
|
||||
document.head.appendChild(link);
|
||||
return () => {
|
||||
document.head.removeChild(link);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{fontWeights.map((weight) => (
|
||||
<div key={`${fontFamily}-${weight}`} style={{ marginBottom: 24 }}>
|
||||
<Divider />
|
||||
<div style={{ marginLeft: 24, marginRight: 24 }}>
|
||||
<h4 style={{marginBottom: 0}}>{fontFamily} {weight}</h4>
|
||||
<div style={{ fontFamily: fontFamily, fontSize: 20, fontWeight: weight, marginLeft: 24 }}>
|
||||
<div style={{ fontFamily: fontFamily }}>The quick brown fox jumped over the lazy dog.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function FontTest() {
|
||||
return (
|
||||
<div>
|
||||
<h4 style={{ marginLeft: 24, marginRight: 24 }}>Font Test</h4>
|
||||
{TypeSample({ fontFamily: "EnglandHand", cssRef: "/fonts/england-hand/stylesheet.css", fontWeights: [400] })}
|
||||
{TypeSample({ fontFamily: "epflul", cssRef: "/fonts/epf/stylesheet.css", fontWeights: [400] })}
|
||||
{TypeSample({ fontFamily: "Nexa", cssRef: "/fonts/nexa/stylesheet.css", fontWeights: [400] })}
|
||||
{TypeSample({ fontFamily: "Pirulen", cssRef: "/fonts/pirulen/stylesheet.css", fontWeights: [400] })}
|
||||
{TypeSample({ fontFamily: "Questrial", cssRef: "/fonts/questrial/stylesheet.css", fontWeights: [400] })}
|
||||
|
||||
{TypeSample({ fontFamily: "Pirulen", cssRef: "/fonts/pirulen/stylesheet.css", fontWeights: [400] })}
|
||||
{TypeSample({ fontFamily: "Pirulen", cssRef: "/fonts/pirulen/stylesheet.css", fontWeights: [400] })}
|
||||
{TypeSample({ fontFamily: "Pirulen", cssRef: "/fonts/pirulen/stylesheet.css", fontWeights: [400] })}
|
||||
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default FontTest;
|
||||
@@ -21,12 +21,13 @@
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import IconButton, {IconButtonProps} from '@mui/material/IconButton';
|
||||
import ToolTipEx from './ToolTipEx';
|
||||
import Typography from "@mui/material/Typography";
|
||||
|
||||
interface IconButtonExProps extends IconButtonProps {
|
||||
tooltip: string;
|
||||
tooltip: React.ReactElement | string;
|
||||
style?: React.CSSProperties;
|
||||
};
|
||||
|
||||
@@ -35,9 +36,10 @@ function IconButtonEx(props: IconButtonExProps) {
|
||||
|
||||
return (
|
||||
<ToolTipEx title={
|
||||
(typeof props.tooltip === 'string') ?
|
||||
(
|
||||
<Typography variant="caption">{tooltip || extra['aria-label'] }</Typography>
|
||||
)
|
||||
): (props.tooltip as React.ReactElement)
|
||||
}
|
||||
>
|
||||
<IconButton {...extra} style={style} />
|
||||
|
||||
@@ -164,7 +164,8 @@ interface PluginGridProps extends WithStyles<typeof pluginGridStyles> {
|
||||
uri?: string;
|
||||
minimumItemWidth?: number;
|
||||
theme: Theme;
|
||||
open: boolean
|
||||
open: boolean;
|
||||
modGuiOnly?: boolean;
|
||||
};
|
||||
|
||||
type PluginGridState = {
|
||||
@@ -526,6 +527,8 @@ export const LoadPluginDialog =
|
||||
|
||||
for (let i = 0; i < plugins.length; ++i) {
|
||||
let plugin = plugins[i];
|
||||
if (this.props.modGuiOnly == true && !plugin.modGui)
|
||||
continue;
|
||||
try {
|
||||
if (filterType === PluginType.Plugin || rootClass.is_type_of(filterType, plugin.plugin_type)) {
|
||||
let score: number = 0;
|
||||
@@ -720,9 +723,15 @@ export const LoadPluginDialog =
|
||||
style={{ overflowX: "hidden", overflowY: "hidden", display: "flex", flexDirection: "column", flexWrap: "nowrap" }}
|
||||
aria-labelledby="select-plugin-dialog-title">
|
||||
<div style={{ display: "flex", flexDirection: "column", flexWrap: "nowrap", height: "100%" }}>
|
||||
<DialogTitle id="select-plugin-dialog-title" style={{ flex: "0 0 auto", padding: "0px", height: 54 }}>
|
||||
<div style={{ display: "flex", flexDirection: "row", paddingTop: 3, paddingBottom: 3, flexWrap: "nowrap", width: "100%", alignItems: "center" }}>
|
||||
<IconButtonEx tooltip="Back" onClick={() => { this.cancel(); }} style={{ flex: "0 0 auto" }} >
|
||||
<DialogTitle id="select-plugin-dialog-title" style={{ flex: "0 0 auto", padding: "0px", height: 54,
|
||||
marginRight: 16
|
||||
}}>
|
||||
<div style={{ display: "flex", flexDirection: "row", paddingTop: 3, paddingBottom: 3, flexWrap: "nowrap",
|
||||
height: 64,
|
||||
width: "100%", alignItems: "center"
|
||||
}}>
|
||||
<IconButtonEx tooltip="Back" onClick={() => { this.cancel(); }}
|
||||
style={{ flex: "0 0 auto", marginRight: 16 }} >
|
||||
<ArrowBackIcon />
|
||||
</IconButtonEx>
|
||||
|
||||
@@ -731,9 +740,9 @@ export const LoadPluginDialog =
|
||||
visibility: (this.state.search_collapsed ? "visible" : "collapse"),
|
||||
width: (this.state.search_collapsed ? undefined : "0px")
|
||||
}}>
|
||||
{this.state.client_width > 520 ? "Select Plugin" : ""}
|
||||
{this.state.client_width > 645 ? "Select Plugin" : ""}
|
||||
</Typography>
|
||||
<div style={{ flex: "1 1 auto" }} >
|
||||
<div style={{ flex: "2 2 auto" }} >
|
||||
<SearchControl collapsed={this.state.search_collapsed} searchText={this.state.search_string}
|
||||
inputRef={this.searchInputRef}
|
||||
showSearchIcon={showSearchIcon}
|
||||
@@ -784,13 +793,13 @@ export const LoadPluginDialog =
|
||||
)}
|
||||
</IconButtonEx>
|
||||
</div>
|
||||
<div style={{ flex: "0 0 160px", marginRight: 24 }} >
|
||||
<div style={{ flex: "0 1 160px", position: "relative", marginRight: 24 }} >
|
||||
|
||||
<TextField select variant="standard"
|
||||
defaultValue={this.state.filterType}
|
||||
key={this.state.filterType}
|
||||
onChange={(e) => { this.onFilterChange(e); }}
|
||||
sx={{ minWidth: 160 }}
|
||||
sx={{ width: "100%",minWidth: "100%" }}
|
||||
>
|
||||
{this.createFilterOptions()}
|
||||
</TextField>
|
||||
|
||||
@@ -26,10 +26,10 @@ interface Deserializable<T> {
|
||||
}
|
||||
|
||||
const noteNames: string[] = [
|
||||
"C","C#","D","Eb","E","F","F#","G","Ab","A","Bb","B","C"
|
||||
"C", "C#", "D", "Eb", "E", "F", "F#", "G", "Ab", "A", "Bb", "B", "C"
|
||||
];
|
||||
|
||||
function semitone12TETValue(value: number) : string {
|
||||
function semitone12TETValue(value: number): string {
|
||||
let iValue = Math.round(value) % 12;
|
||||
return noteNames[iValue];
|
||||
|
||||
@@ -313,10 +313,85 @@ export class UiFileProperty {
|
||||
useLegacyModDirectory: boolean = false;
|
||||
|
||||
};
|
||||
|
||||
export class ModGuiPort {
|
||||
deserialize(input: any): ModGuiPort {
|
||||
this.index = input.index;
|
||||
this.symbol = input.symbol;
|
||||
this.name = input.name;
|
||||
return this;
|
||||
}
|
||||
static deserialize_array(input: any): ModGuiPort[] {
|
||||
let result: ModGuiPort[] = [];
|
||||
for (let i = 0; i < input.length; ++i) {
|
||||
result[i] = new ModGuiPort().deserialize(input[i]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
index: number = -1;
|
||||
symbol: string = "";
|
||||
name: string = "";
|
||||
};
|
||||
export class ModGui {
|
||||
deserialize(input: any): ModGui {
|
||||
this.pluginUri = input.pluginUri;
|
||||
this.resourceDirectory = input.resourceDirectory;
|
||||
this.iconTemplate = input.iconTemplate ?? "";
|
||||
this.settingsTemplate = input.settingsTemplate;
|
||||
this.javascript = input.javascript;
|
||||
this.stylesheet = input.stylesheet ?? "";
|
||||
this.screenshot = input.screenshot;
|
||||
this.thumbnail = input.thumbnail;
|
||||
this.discussionUrl = input.discussionUrl;
|
||||
this.documentationUrl = input.documentationUrl;
|
||||
this.brand = input.brand;
|
||||
this.label = input.label;
|
||||
this.model = input.model;
|
||||
this.panel = input.panel;
|
||||
this.color = input.color;
|
||||
this.knob = input.knob;
|
||||
|
||||
if (input.ports) {
|
||||
this.ports = ModGuiPort.deserialize_array(input.ports);
|
||||
} else {
|
||||
this.ports = [];
|
||||
}
|
||||
return this;
|
||||
}
|
||||
static serialize_array(input: any): ModGui[] {
|
||||
let result: ModGui[] = [];
|
||||
for (let i = 0; i < input.length; ++i) {
|
||||
result[i] = new ModGui().deserialize(input[i]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
pluginUri = "";
|
||||
resourceDirectory = "";
|
||||
iconTemplate = "";
|
||||
settingsTemplate = "";
|
||||
javascript = "";
|
||||
stylesheet = "";
|
||||
screenshot = "";
|
||||
thumbnail = "";
|
||||
discussionUrl = "";
|
||||
documentationUrl = "";
|
||||
brand = "";
|
||||
label = "";
|
||||
model = "";
|
||||
panel = "";
|
||||
color = "";
|
||||
knob = "";
|
||||
ports: ModGuiPort[] = [];
|
||||
|
||||
|
||||
};
|
||||
|
||||
export class Lv2Plugin implements Deserializable<Lv2Plugin> {
|
||||
deserialize(input: any): Lv2Plugin {
|
||||
this.uri = input.uri;
|
||||
this.name = input.name;
|
||||
this.minorVersion = input.minorVersion ? input.minorVersion : 0;
|
||||
this.microVersion = input.microVersion ? input.microVersion : 0;
|
||||
this.brand = input.name ? input.name : "";
|
||||
this.label = input.label ? input.label : this.name;
|
||||
this.plugin_class = input.plugin_class;
|
||||
@@ -344,12 +419,18 @@ export class Lv2Plugin implements Deserializable<Lv2Plugin> {
|
||||
} else {
|
||||
this.uiPortNotifications = [];
|
||||
}
|
||||
this.modGui = null;
|
||||
if (input.modGui) {
|
||||
this.modGui = new ModGui().deserialize(input.modGui);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
static EmptyFeatures: string[] = [];
|
||||
|
||||
uri: string = "";
|
||||
name: string = "";
|
||||
minorVersion: number = 0;
|
||||
microVersion: number = 0;
|
||||
brand: string = "";
|
||||
label: string = "";
|
||||
plugin_class: string = "";
|
||||
@@ -364,6 +445,7 @@ export class Lv2Plugin implements Deserializable<Lv2Plugin> {
|
||||
fileProperties: UiFileProperty[] = [];
|
||||
frequencyPlots: UiFrequencyPlot[] = [];
|
||||
uiPortNotifications: UiPropertyNotification[] = [];
|
||||
modGui: ModGui | null = null;
|
||||
}
|
||||
|
||||
|
||||
@@ -459,10 +541,12 @@ export enum ControlType {
|
||||
Vu,
|
||||
Progress,
|
||||
DbVu,
|
||||
OutputText
|
||||
OutputText,
|
||||
|
||||
BypassLight
|
||||
}
|
||||
|
||||
const textUnits : Set<Units> = new Set<Units>([
|
||||
const textUnits: Set<Units> = new Set<Units>([
|
||||
Units.bar,
|
||||
Units.beat,
|
||||
Units.bpm,
|
||||
@@ -478,13 +562,12 @@ const textUnits : Set<Units> = new Set<Units>([
|
||||
Units.semitone12TET
|
||||
]);
|
||||
|
||||
function displayUnitAsText(unit: Units): boolean
|
||||
{
|
||||
function displayUnitAsText(unit: Units): boolean {
|
||||
return textUnits.has(unit);
|
||||
}
|
||||
|
||||
export class UiControl implements Deserializable<UiControl> {
|
||||
deserialize(input: any): UiControl {
|
||||
deserialize(input: any): UiControl {
|
||||
this.symbol = input.symbol;
|
||||
this.name = input.name;
|
||||
this.index = input.index;
|
||||
@@ -531,12 +614,11 @@ deserialize(input: any): UiControl {
|
||||
this.controlType = ControlType.Progress;
|
||||
} else if (this.enumeration_property) {
|
||||
this.controlType = ControlType.OutputText;
|
||||
} else if (displayUnitAsText(this.units))
|
||||
{
|
||||
} else if (displayUnitAsText(this.units)) {
|
||||
this.controlType = ControlType.OutputText
|
||||
} else {
|
||||
this.controlType = ControlType.Vu;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (this.isValidEnumeration()) {
|
||||
this.controlType = ControlType.Select;
|
||||
@@ -548,10 +630,8 @@ deserialize(input: any): UiControl {
|
||||
this.controlType = ControlType.OnOffSwitch;
|
||||
}
|
||||
}
|
||||
if (this.is_input)
|
||||
{
|
||||
if (this.pipedal_graphicEq)
|
||||
{
|
||||
if (this.is_input) {
|
||||
if (this.pipedal_graphicEq) {
|
||||
this.controlType = ControlType.GraphicEq;
|
||||
}
|
||||
else if (this.mod_momentaryOnByDefault) {
|
||||
@@ -560,7 +640,7 @@ deserialize(input: any): UiControl {
|
||||
this.controlType = ControlType.Momentary;
|
||||
} else if (this.trigger_property) {
|
||||
this.controlType = ControlType.Trigger;
|
||||
}
|
||||
}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
@@ -654,7 +734,7 @@ deserialize(input: any): UiControl {
|
||||
isAbToggle(): boolean {
|
||||
return this.controlType === ControlType.ABSwitch;
|
||||
}
|
||||
isGraphicEq() : boolean {
|
||||
isGraphicEq(): boolean {
|
||||
return this.controlType === ControlType.GraphicEq;
|
||||
}
|
||||
isSelect(): boolean {
|
||||
@@ -739,15 +819,12 @@ deserialize(input: any): UiControl {
|
||||
}
|
||||
}
|
||||
|
||||
if (this.units === Units.s)
|
||||
{
|
||||
if (this.units === Units.s) {
|
||||
let iValue = Math.round(value);
|
||||
if (iValue >= 60)
|
||||
{
|
||||
let minutes = Math.floor(iValue/60);
|
||||
if (iValue >= 60) {
|
||||
let minutes = Math.floor(iValue / 60);
|
||||
let seconds = iValue % 60;
|
||||
if (seconds < 10)
|
||||
{
|
||||
if (seconds < 10) {
|
||||
return minutes + ":0" + seconds;
|
||||
|
||||
}
|
||||
@@ -756,11 +833,10 @@ deserialize(input: any): UiControl {
|
||||
return this.formatShortValue(value);
|
||||
}
|
||||
}
|
||||
if (this.units === Units.semitone12TET)
|
||||
{
|
||||
if (this.units === Units.semitone12TET) {
|
||||
return semitone12TETValue(value);
|
||||
}
|
||||
|
||||
|
||||
let text = this.formatShortValue(value);
|
||||
|
||||
switch (this.units) {
|
||||
@@ -831,11 +907,45 @@ deserialize(input: any): UiControl {
|
||||
|
||||
}
|
||||
|
||||
export class Lv2PatchPropertyInfo {
|
||||
deserialize(input: any): Lv2PatchPropertyInfo {
|
||||
this.uri = input.uri;
|
||||
this.writable = input.writable;
|
||||
this.readable = input.readable;
|
||||
this.label = input.label;
|
||||
this.index = input.index;
|
||||
this.type = input.type;
|
||||
this.comment = input.comment;
|
||||
this.shortName = input.shortName;
|
||||
this.fileTypes = input.fileTypes;
|
||||
this.supportedExtensions = input.supportedExtensions;
|
||||
return this;
|
||||
}
|
||||
static deserialize_array(input: any): Lv2PatchPropertyInfo[] {
|
||||
let result: Lv2PatchPropertyInfo[] = [];
|
||||
for (let i = 0; i < input.length; ++i) {
|
||||
result[i] = new Lv2PatchPropertyInfo().deserialize(input[i]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
uri: string = "";
|
||||
writable : boolean = false;
|
||||
readable: boolean = false;
|
||||
label: string = "";
|
||||
index: number = -1;
|
||||
type: string = "";
|
||||
comment: string = "";
|
||||
shortName: string = "";
|
||||
fileTypes: string[] = [];
|
||||
supportedExtensions: string[] = [];
|
||||
};
|
||||
|
||||
export class UiPlugin implements Deserializable<UiPlugin> {
|
||||
deserialize(input: any): UiPlugin {
|
||||
this.uri = input.uri;
|
||||
this.name = input.name;
|
||||
this.minorVersion = input.minorVersion ? input.minorVersion : 0;
|
||||
this.microVersion = input.microVersion ? input.microVersion : 0;
|
||||
this.brand = input.brand ? input.brand : "";
|
||||
this.label = input.label ? input.label : this.name;
|
||||
this.plugin_type = input.plugin_type as PluginType;
|
||||
@@ -861,6 +971,11 @@ export class UiPlugin implements Deserializable<UiPlugin> {
|
||||
}
|
||||
|
||||
this.is_vst3 = input.is_vst3;
|
||||
this.modGui = null;
|
||||
if (input.modGui) {
|
||||
this.modGui = new ModGui().deserialize(input.modGui);
|
||||
}
|
||||
this.patchProperties = Lv2PatchPropertyInfo.deserialize_array(input.patchProperties ?? []);
|
||||
return this;
|
||||
|
||||
}
|
||||
@@ -908,6 +1023,8 @@ export class UiPlugin implements Deserializable<UiPlugin> {
|
||||
|
||||
uri: string = "";
|
||||
name: string = "";
|
||||
minorVersion: number = 0;
|
||||
microVersion: number = 0;
|
||||
brand: string = "";
|
||||
label: string = "";
|
||||
plugin_type: PluginType = PluginType.InvalidPlugin;
|
||||
@@ -924,6 +1041,8 @@ export class UiPlugin implements Deserializable<UiPlugin> {
|
||||
fileProperties: UiFileProperty[] = [];
|
||||
frequencyPlots: UiFrequencyPlot[] = [];
|
||||
is_vst3: boolean = false;
|
||||
modGui: ModGui | null = null; // null if no mod gui.
|
||||
patchProperties: Lv2PatchPropertyInfo[] = [];
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -58,6 +58,8 @@ import Snapshot3Icon from "./svg/snapshot_3.svg?react";
|
||||
import Snapshot4Icon from "./svg/snapshot_4.svg?react";
|
||||
import Snapshot5Icon from "./svg/snapshot_5.svg?react";
|
||||
import Snapshot6Icon from "./svg/snapshot_6.svg?react";
|
||||
import ModUiIcon from './svg/mod_ui.svg?react';
|
||||
import PipedalUiIcon from './svg/pp_ui.svg?react';
|
||||
|
||||
import SnapshotDialog from './SnapshotDialog';
|
||||
import { css } from '@emotion/react';
|
||||
@@ -128,6 +130,8 @@ interface MainState {
|
||||
showMidiBindingsDialog: boolean;
|
||||
screenHeight: number;
|
||||
displayNameDialogOpen: boolean;
|
||||
showModUi: boolean;
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -167,7 +171,8 @@ export const MainPage =
|
||||
displayAuthor: this.getDisplayAuthor(),
|
||||
horizontalScrollLayout: this.windowSize.height < HORIZONTAL_CONTROL_SCROLL_HEIGHT_BREAK,
|
||||
showMidiBindingsDialog: false,
|
||||
screenHeight: this.windowSize.height
|
||||
screenHeight: this.windowSize.height,
|
||||
showModUi: false
|
||||
|
||||
|
||||
};
|
||||
@@ -375,7 +380,7 @@ export const MainPage =
|
||||
if (pedalboardItem === null) return "";
|
||||
return pedalboardItem.uri;
|
||||
}
|
||||
titleBar(pedalboardItem: PedalboardItem | null): React.ReactNode {
|
||||
titleBar(pedalboardItem: PedalboardItem | null,canShowModUi: boolean): React.ReactNode {
|
||||
let title = "";
|
||||
let author = "";
|
||||
let infoPluginUri = "";
|
||||
@@ -441,7 +446,7 @@ export const MainPage =
|
||||
{canEditTitle ? (
|
||||
<ButtonBase
|
||||
style={{ borderRadius: "6px", width: "100%", paddingLeft: 8, paddingRight: 8, height: 48, textTransform: "none" }}
|
||||
onClick={()=> {
|
||||
onClick={() => {
|
||||
this.handleEditPluginDisplayName();
|
||||
}}
|
||||
>
|
||||
@@ -453,7 +458,7 @@ export const MainPage =
|
||||
</span>
|
||||
</ButtonBase>)
|
||||
: (
|
||||
<div style={{ flex: "0 1 auto", paddingLeft: 8, paddingRight: 8,minWidth: 0, overflow: "hidden", textOverflow: "ellipsis" }}>
|
||||
<div style={{ flex: "0 1 auto", paddingLeft: 8, paddingRight: 8, minWidth: 0, overflow: "hidden", textOverflow: "ellipsis" }}>
|
||||
<span className={classes.title}>{title}</span>
|
||||
{this.state.displayAuthor && (
|
||||
<span className={classes.author}>{author}</span>
|
||||
@@ -469,6 +474,32 @@ export const MainPage =
|
||||
<PluginPresetSelector pluginUri={presetsUri} instanceId={pedalboardItem?.instanceId ?? 0}
|
||||
/>
|
||||
</div>
|
||||
{canShowModUi && (
|
||||
<div style={{ flex: "0 0 auot" }}>
|
||||
<IconButtonEx tooltip={(
|
||||
<div>
|
||||
<Typography variant="body2" >
|
||||
{this.state.showModUi ? "PiPedal UI" : "MOD UI"}</Typography>
|
||||
<Divider />
|
||||
<Typography variant="caption">Use MOD UI or PiPedal UI for plugin</Typography>
|
||||
</div>
|
||||
|
||||
)}
|
||||
onClick={() => {
|
||||
this.setState({ showModUi: !this.state.showModUi });
|
||||
}}
|
||||
size="large">
|
||||
{!this.state.showModUi ?
|
||||
(
|
||||
<ModUiIcon style={{ height: 24, width: 24, fill: this.props.theme.palette.text.primary, opacity: 0.6 }} />
|
||||
) : (
|
||||
<PipedalUiIcon style={{ height: 24, width: 24, fill: this.props.theme.palette.text.primary, opacity: 0.6 }} />
|
||||
)
|
||||
}
|
||||
</IconButtonEx>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -494,6 +525,15 @@ export const MainPage =
|
||||
}
|
||||
}
|
||||
|
||||
canShowModUi(pedalboardItem: PedalboardItem): boolean {
|
||||
let pluginInfo = this.model.getUiPlugin(pedalboardItem.uri);
|
||||
if (pluginInfo === null) {
|
||||
return false;
|
||||
}
|
||||
return pluginInfo.modGui !== null;
|
||||
|
||||
}
|
||||
|
||||
render() {
|
||||
const classes = withStyles.getClasses(this.props);
|
||||
let pedalboard = this.model.pedalboard.get();
|
||||
@@ -508,8 +548,9 @@ export const MainPage =
|
||||
let instanceId = -1;
|
||||
let missing = false;
|
||||
let pluginUri = "#error";
|
||||
|
||||
let canShowModUi = false;
|
||||
if (pedalboardItem) {
|
||||
canShowModUi = this.canShowModUi(pedalboardItem);
|
||||
canDelete = pedalboard.canDeleteItem(pedalboardItem.instanceId);
|
||||
instanceId = pedalboardItem.instanceId;
|
||||
if (pedalboardItem.isEmpty()) {
|
||||
@@ -570,7 +611,7 @@ export const MainPage =
|
||||
</div>
|
||||
{
|
||||
(!this.state.splitControlBar || !this.props.enableStructureEditing)
|
||||
&& this.titleBar(pedalboardItem)
|
||||
&& this.titleBar(pedalboardItem, canShowModUi)
|
||||
}
|
||||
<div style={{ flex: "1 1 1px" }}>
|
||||
|
||||
@@ -642,7 +683,7 @@ export const MainPage =
|
||||
this.state.splitControlBar && this.props.enableStructureEditing && (
|
||||
<div className={classes.splitControlBar}>
|
||||
{
|
||||
this.titleBar(pedalboardItem)
|
||||
this.titleBar(pedalboardItem, canShowModUi)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
@@ -657,7 +698,11 @@ export const MainPage =
|
||||
|
||||
) :
|
||||
(
|
||||
GetControlView(pedalboardItem)
|
||||
GetControlView(pedalboardItem,this.state.showModUi && canShowModUi,
|
||||
(instanceId: number, showModGui: boolean) => {
|
||||
this.setState({ showModUi: showModGui });
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
</div>
|
||||
@@ -689,7 +734,7 @@ export const MainPage =
|
||||
this.setState({ displayNameDialogOpen: false });
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Robin E. R. Davies
|
||||
* All rights reserved.
|
||||
|
||||
* 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 { Component, ErrorInfo, ReactNode } from "react";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import Button from "@mui/material/Button";
|
||||
|
||||
import { UiPlugin } from "./Lv2Plugin";
|
||||
|
||||
interface Props {
|
||||
plugin: UiPlugin | null;
|
||||
children?: ReactNode;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
interface State {
|
||||
hasError: boolean;
|
||||
message: string;
|
||||
}
|
||||
|
||||
class ModGuiErrorBoundary extends Component<Props, State> {
|
||||
public state: State = {
|
||||
hasError: false,
|
||||
message: "",
|
||||
};
|
||||
|
||||
public static getDerivedStateFromError(error: Error): State {
|
||||
// Update state so the next render will show the fallback UI.
|
||||
return { hasError: true, message: error.message };
|
||||
}
|
||||
|
||||
public componentDidCatch(error: Error, errorInfo: ErrorInfo) {
|
||||
console.error("ModGUI rendering error: ", error);
|
||||
console.error("Stack trace: " , errorInfo.componentStack);
|
||||
}
|
||||
|
||||
public render() {
|
||||
if (this.state.hasError) {
|
||||
return (
|
||||
<div style={{
|
||||
position: "absolute",left: 0, right: 0, top: 0, bottom: 0,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "stretch",
|
||||
}}>
|
||||
<div style={{ flex: "1 1 auto" }} />
|
||||
<div style={{display: "flex", flexDirection: "row", alignItems: "stretch", justifyContent: "stretch"}}>
|
||||
<div style={{ flex: "1 1 auto" }} />
|
||||
<div style={{maxWidth: 600, margin: 24}}>
|
||||
<Typography variant="h6" style={{ marginBottom: 8 }} >
|
||||
{ this.props.plugin ?
|
||||
`Error Loading '${this.props.plugin.name}'` :
|
||||
`Error Loading UI`
|
||||
}
|
||||
</Typography>
|
||||
|
||||
<Typography variant="body2" style={{marginBottom:16}} >
|
||||
{this.state.message}
|
||||
</Typography>
|
||||
<div style={{alignSelf: "flex-end", marginBottom: 16, marginRight: 24, display: "flex", flexFlow: "row nowrap", justifyContent: "flex-end" }}>
|
||||
<Button
|
||||
style={{alignSelf: "flex-end"}}
|
||||
variant="contained"
|
||||
onClick={() => {
|
||||
this.props.onClose();
|
||||
|
||||
}}
|
||||
>Close
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ flex: "1 1 auto" }} />
|
||||
</div>
|
||||
<div style={{ flex: "2 2 auto" }} />
|
||||
|
||||
</div>
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
|
||||
export default ModGuiErrorBoundary;
|
||||
@@ -0,0 +1,346 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Robin E. R. Davies
|
||||
* All rights reserved.
|
||||
|
||||
* 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,
|
||||
* 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 Divider from '@mui/material/Divider';
|
||||
import { PiPedalModel, PiPedalModelFactory, MonitorPortHandle, State, ListenHandle, PatchPropertyListener } from './PiPedalModel';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import CircularProgress from '@mui/material/CircularProgress';
|
||||
import LoadPluginDialog from './LoadPluginDialog';
|
||||
import ButtonEx from './ButtonEx';
|
||||
import Typography from '@mui/material/Typography/Typography';
|
||||
import ModGuiHost, { IModGuiHostSite } from './ModGuiHost';
|
||||
import { UiPlugin } from './Lv2Plugin';
|
||||
import ModGuiErrorBoundary from './ModGuiErrorBoundary';
|
||||
|
||||
class MyPortHandle implements MonitorPortHandle {
|
||||
private static nextHandle: number = 1;
|
||||
constructor() {
|
||||
this.handle = MyPortHandle.nextHandle++;
|
||||
}
|
||||
handle: number;
|
||||
};
|
||||
|
||||
type MonitorCallback = (value: number) => void;
|
||||
|
||||
class ValueEntry {
|
||||
constructor(defaultValue: number) {
|
||||
this.value = defaultValue;
|
||||
}
|
||||
private listeners: { [handle: number]: MonitorCallback } = {};
|
||||
|
||||
monitor(interval: number, callback: MonitorCallback): MyPortHandle {
|
||||
let handle = new MyPortHandle();
|
||||
this.listeners[handle.handle] = callback;
|
||||
return handle;
|
||||
}
|
||||
unmonitor(handle: MonitorPortHandle) {
|
||||
let h = (handle as MyPortHandle).handle;
|
||||
// remove entry h from listeners
|
||||
if (!(h in this.listeners)) {
|
||||
throw new Error("Invalid handle: " + h);
|
||||
}
|
||||
// remove the callback from the listeners
|
||||
delete this.listeners[h];
|
||||
}
|
||||
setValue(value: number) {
|
||||
if (value !== this.value) {
|
||||
this.value = value;
|
||||
// notify all listeners
|
||||
for (let handle in this.listeners) {
|
||||
this.listeners[handle](value);
|
||||
}
|
||||
}
|
||||
}
|
||||
value: number;
|
||||
};
|
||||
|
||||
class PatchListenerEntry {
|
||||
constructor(instanceId: number, propertyUri: string) {
|
||||
this.instanceId = instanceId;
|
||||
this.propertyUri = propertyUri;
|
||||
}
|
||||
instanceId: number;
|
||||
propertyUri: string;
|
||||
value: any = "";
|
||||
listeners: { handle: number, listener: PatchPropertyListener}[] = [];;
|
||||
};
|
||||
|
||||
|
||||
class ValueHandler {
|
||||
|
||||
private valueDictionary: { [symbol: string]: ValueEntry } = {};
|
||||
private handleToValueEnry: { [handle: number]: ValueEntry } = {};
|
||||
|
||||
constructor(instanceId: number, plugin: UiPlugin) {
|
||||
for (let control of plugin.controls) {
|
||||
this.valueDictionary[control.symbol] = new ValueEntry(control.default_value);
|
||||
}
|
||||
this.valueDictionary["_bypass"] = new ValueEntry(0);
|
||||
}
|
||||
monitorPort(instanceId: number, symbol: string, interval: number, callback: MonitorCallback): MonitorPortHandle {
|
||||
let valueEntry = this.valueDictionary[symbol];
|
||||
|
||||
let portHandle = valueEntry.monitor(interval, callback);
|
||||
this.handleToValueEnry[(portHandle as MyPortHandle).handle] = valueEntry;
|
||||
callback(valueEntry.value);
|
||||
return portHandle;
|
||||
}
|
||||
unmonitorPort(handle: MonitorPortHandle): void {
|
||||
let h = (handle as MyPortHandle).handle;
|
||||
if (!(h in this.handleToValueEnry)) {
|
||||
throw new Error("Invalid handle: " + h);
|
||||
}
|
||||
let valueEntry = this.handleToValueEnry[h];
|
||||
valueEntry.unmonitor(handle);
|
||||
delete this.handleToValueEnry[h];
|
||||
}
|
||||
setValue(instanceId: number, symbol: string, value: number) {
|
||||
// add a delay just for funsies.
|
||||
window.setTimeout(() => {
|
||||
if (!(symbol in this.valueDictionary)) {
|
||||
throw new Error("Invalid symbol: " + symbol);
|
||||
}
|
||||
let valueEntry = this.valueDictionary[symbol];
|
||||
valueEntry.setValue(value);
|
||||
}, 100);
|
||||
}
|
||||
getValue(instanceId: number, symbol: string): number {
|
||||
if (!(symbol in this.valueDictionary)) {
|
||||
throw new Error("Invalid symbol: " + symbol);
|
||||
}
|
||||
let valueEntry = this.valueDictionary[symbol];
|
||||
return valueEntry.value;
|
||||
}
|
||||
private nextListenHandle: number = 1;
|
||||
|
||||
|
||||
private patchListeners: PatchListenerEntry[] = [];
|
||||
|
||||
monitorPatchProperty(instanceId: number, propertyUri: string, onReceived: PatchPropertyListener): ListenHandle {
|
||||
let h = this.nextListenHandle++;
|
||||
for (let entry of this.patchListeners) {
|
||||
if (entry.instanceId === instanceId && entry.propertyUri === propertyUri) {
|
||||
// already listening to this property
|
||||
entry.listeners.push({ handle: h, listener: onReceived });
|
||||
onReceived(instanceId,propertyUri,entry.value);
|
||||
return { _handle: h };
|
||||
}
|
||||
}
|
||||
let entry = new PatchListenerEntry(instanceId, propertyUri);
|
||||
this.patchListeners.push(entry);
|
||||
entry.listeners.push({ handle: h, listener: onReceived });
|
||||
onReceived(instanceId, propertyUri, entry.value);
|
||||
return {_handle: h};
|
||||
}
|
||||
cancelMonitorPatchProperty(listenHandle: ListenHandle): void {
|
||||
for (let i = 0; i < this.patchListeners.length; ++i) {
|
||||
let entry = this.patchListeners[i];
|
||||
for (let j = 0; j < entry.listeners.length; ++j) {
|
||||
if (entry.listeners[j].handle === listenHandle._handle) {
|
||||
// found the listener, remove it
|
||||
entry.listeners.splice(j, 1);
|
||||
if (entry.listeners.length === 0) {
|
||||
// no more listeners, remove the entry
|
||||
this.patchListeners.splice(i, 1);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
getPatchProperty(instanceId: number, uri: string): Promise<any> {
|
||||
let promise = new Promise<any>((resolve, reject) => {
|
||||
window.setTimeout(() => {
|
||||
for (let entry of this.patchListeners) {
|
||||
if (entry.instanceId === instanceId && entry.propertyUri === uri) {
|
||||
window.setTimeout(() => {
|
||||
// simulate a delay for getting the property
|
||||
entry.listeners.forEach(l => l.listener(instanceId, uri, entry.value));
|
||||
}, 100);
|
||||
return resolve(entry.value);
|
||||
}
|
||||
}
|
||||
reject(new Error(`Patch property not found: ${uri} for instance ${instanceId}`));
|
||||
},50);
|
||||
});
|
||||
return promise;
|
||||
}
|
||||
setPatchProperty(instanceId: number, uri: string, value: any): Promise<boolean> {
|
||||
// Implementation here
|
||||
for (let entry of this.patchListeners) {
|
||||
if (entry.instanceId === instanceId && entry.propertyUri === uri) {
|
||||
entry.value = value;
|
||||
// notify all listeners
|
||||
entry.listeners.forEach(l => l.listener(instanceId, uri, value));
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
}
|
||||
// if we reach here, the property was not found
|
||||
return Promise.reject("Not found.");
|
||||
}
|
||||
};
|
||||
|
||||
function ModGuiTest() {
|
||||
const [pluginUrl, setPluginUrl] = React.useState("");
|
||||
const [uiPlugin, setUiPlugin] = React.useState<UiPlugin | null>(null);
|
||||
const [pluginDialogOpen, setPluginDialogOpen] = React.useState(false);
|
||||
const [loading, setLoading] = React.useState(true);
|
||||
const [valueHandler, setValueHandler] = React.useState<ValueHandler | null>(null);
|
||||
|
||||
const [modGuiHostSite] = React.useState<IModGuiHostSite>({
|
||||
monitorPort: (instanceId: number, symbol: string, interval: number, callback: (value: number) => void) => {
|
||||
if (!valueHandler) {
|
||||
throw new Error("ValueHandler is not set");
|
||||
}
|
||||
return valueHandler.monitorPort(instanceId, symbol, interval, callback);
|
||||
},
|
||||
unmonitorPort: (handle) => {
|
||||
if (!valueHandler) {
|
||||
throw new Error("ValueHandler is not set");
|
||||
}
|
||||
valueHandler.unmonitorPort(handle);
|
||||
},
|
||||
setPedalboardControl: (instanceId, symbol, value) => {
|
||||
if (!valueHandler) {
|
||||
throw new Error("ValueHandler is not set");
|
||||
}
|
||||
valueHandler.setValue(instanceId, symbol, value);
|
||||
},
|
||||
monitorPatchProperty(instanceId: number, propertyUri: string, onReceived: PatchPropertyListener): ListenHandle {
|
||||
if (!valueHandler) {
|
||||
throw new Error("ValueHandler is not set");
|
||||
}
|
||||
|
||||
return valueHandler.monitorPatchProperty(instanceId, propertyUri, onReceived);
|
||||
},
|
||||
cancelMonitorPatchProperty: (listenHandle) => {
|
||||
if (!valueHandler) {
|
||||
throw new Error("ValueHandler is not set");
|
||||
}
|
||||
valueHandler.cancelMonitorPatchProperty(listenHandle);
|
||||
},
|
||||
getPatchProperty: async (instanceId, uri) => {
|
||||
if (!valueHandler) {
|
||||
throw new Error("ValueHandler is not set");
|
||||
}
|
||||
return valueHandler.getPatchProperty(instanceId, uri) as Promise<any>;
|
||||
},
|
||||
setPatchProperty: async (instanceId, uri, value) => {
|
||||
if (!valueHandler) {
|
||||
throw new Error("ValueHandler is not set");
|
||||
}
|
||||
return valueHandler.setPatchProperty(instanceId, uri, value) as Promise<boolean>;
|
||||
}
|
||||
});
|
||||
|
||||
let model: PiPedalModel = PiPedalModelFactory.getInstance();
|
||||
|
||||
React.useEffect(() => {
|
||||
|
||||
const onStateChange = (state: State) => {
|
||||
if (state == State.Ready) {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
model.state.addOnChangedHandler(onStateChange);
|
||||
|
||||
return () => {
|
||||
model.state.removeOnChangedHandler(onStateChange);
|
||||
};
|
||||
}, [])
|
||||
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
display: "flex", flexFlow: "column nowrap", alignItems: "stretch", justifyContent: "stretch",
|
||||
position: "absolute", top: 0, left: 0, width: "100%", height: "100%", backgroundColor: (theme.palette as any).mainBackground
|
||||
}}>
|
||||
|
||||
<div style={{
|
||||
position: "absolute", top: 0, left: 0, right: 0,
|
||||
bottom: 0, zIndex: 1000,
|
||||
display: loading ? "flex" : "none",
|
||||
alignItems: "center", justifyContent: "center",
|
||||
background: "rgba(0.5,0.5,0.5, 0.9)"
|
||||
}}>
|
||||
<CircularProgress style={{ color: theme.palette.text.secondary }} />
|
||||
</div>
|
||||
|
||||
|
||||
<div style={{
|
||||
flex: "0 0 auto", display: "flex", flexFlow: "row nowrap",
|
||||
alignItems: "center", gap: 16, paddingLeft: 32, paddingRight: 32, paddingTop: 16, paddingBottom: 16,
|
||||
}}>
|
||||
<Typography noWrap variant="h6" style={{ flex: "1 0 auto" }}>ModGUI Test</Typography>
|
||||
<Typography noWrap variant="body2" style={{ flex: "0 1 auto", }}>
|
||||
{pluginUrl ? pluginUrl : "No plugin loaded"}
|
||||
</Typography>
|
||||
<ButtonEx style={{ flex: "0 0 auto" }} variant="contained" tooltip="Select plugin" onClick={() => setPluginDialogOpen(true)} >
|
||||
Load
|
||||
</ButtonEx>
|
||||
</div>
|
||||
|
||||
<Divider />
|
||||
|
||||
{uiPlugin && (
|
||||
<div style={{ flex: "1 1 auto", position: "relative" }}>
|
||||
<ModGuiErrorBoundary plugin={uiPlugin} onClose={() => {
|
||||
setPluginUrl("");
|
||||
setUiPlugin(null);
|
||||
setValueHandler(null);
|
||||
}} >
|
||||
|
||||
<ModGuiHost instanceId={-1} plugin={uiPlugin}
|
||||
onClose={() => {
|
||||
setPluginUrl("");
|
||||
setUiPlugin(null);
|
||||
}}
|
||||
hostSite={modGuiHostSite}
|
||||
/>
|
||||
</ModGuiErrorBoundary>
|
||||
</div>
|
||||
)}
|
||||
{pluginDialogOpen &&
|
||||
<LoadPluginDialog
|
||||
open={pluginDialogOpen}
|
||||
onOk={(url) => {
|
||||
setPluginUrl(url as string);
|
||||
let uiPlugin = model.getUiPlugin(url);
|
||||
setUiPlugin(uiPlugin);
|
||||
if (uiPlugin) {
|
||||
setValueHandler(new ValueHandler(-1, uiPlugin));
|
||||
}
|
||||
setPluginDialogOpen(false);
|
||||
}}
|
||||
onCancel={() => setPluginDialogOpen(false)}
|
||||
uri={pluginUrl}
|
||||
modGuiOnly={true}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ModGuiTest;
|
||||
@@ -0,0 +1,105 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2022 Robin E. R. 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 ArrowBackIcon from '@mui/icons-material/ArrowBack';
|
||||
import IconButtonEx from './IconButtonEx';
|
||||
import Button from '@mui/material/Button';
|
||||
import Toolbar from '@mui/material/Toolbar';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import DialogEx from './DialogEx';
|
||||
import DialogTitle from '@mui/material/DialogTitle';
|
||||
import DialogActions from '@mui/material/DialogActions';
|
||||
import DialogContent from '@mui/material/DialogContent';
|
||||
|
||||
import ReportGmailerrorredIcon from '@mui/icons-material/ReportGmailerrorred';
|
||||
|
||||
export interface OkDialogProps {
|
||||
open: boolean,
|
||||
title?: string,
|
||||
text: string,
|
||||
okButtonText?: string,
|
||||
onClose: () => void
|
||||
};
|
||||
|
||||
export interface OkDialogState {
|
||||
};
|
||||
|
||||
export default class OkDialog extends React.Component<OkDialogProps, OkDialogState> {
|
||||
|
||||
|
||||
constructor(props: OkDialogProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
let props = this.props;
|
||||
let { open, okButtonText, text, onClose } = props;
|
||||
|
||||
const handleClose = () => {
|
||||
onClose();
|
||||
};
|
||||
|
||||
const handleOk = () => {
|
||||
onClose();
|
||||
}
|
||||
return (
|
||||
<DialogEx tag="alert" open={open} onClose={handleClose}
|
||||
onEnterKey={handleOk}
|
||||
maxWidth="sm"
|
||||
style={{ userSelect: "none" }}
|
||||
>
|
||||
{props.title && (
|
||||
<DialogTitle id="alert-dialog-title" style={{ userSelect: "none", padding: 0, margin: 0 }}>
|
||||
<Toolbar style={{paddingLeft: 24, paddingRight: 24,margin: 0}} >
|
||||
<IconButtonEx
|
||||
tooltip="Back" edge="start" color="inherit" onClick={()=>{this.props.onClose();}} aria-label="back"
|
||||
>
|
||||
<ArrowBackIcon />
|
||||
</IconButtonEx>
|
||||
|
||||
<Typography noWrap variant="body1" style={{ flex: "1 1 auto" }}>{props.title}</Typography>
|
||||
</Toolbar>
|
||||
</DialogTitle>
|
||||
)}
|
||||
|
||||
<DialogContent >
|
||||
<div style={{ display: "flex", flexFlow: "row nowrap", gap: 16, marginLeft: 32, marginRight: 32, alignItems: "start" }}>
|
||||
<ReportGmailerrorredIcon style={{ color: "var(--color-error)", fontSize: 48, flexShrink: 0, opacity: 0.8 }} />
|
||||
<Typography variant="body2" style={{ marginTop:16}}
|
||||
>{text}</Typography>
|
||||
</div>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
|
||||
<Button variant="dialogPrimary" onClick={handleOk} style={{marginRight: 24}} >
|
||||
{okButtonText ? okButtonText : "OK"}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</DialogEx>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -416,6 +416,7 @@ export class PiPedalModel //implements PiPedalModel
|
||||
|
||||
socketServerUrl: string = "";
|
||||
varServerUrl: string = "";
|
||||
modResourcesUrl: string = "";
|
||||
lv2Path: string = "";
|
||||
webSocket?: PiPedalSocket;
|
||||
|
||||
@@ -972,13 +973,17 @@ export class PiPedalModel //implements PiPedalModel
|
||||
// anything could have changed while we were disconnected.
|
||||
await this.loadServerState();
|
||||
}
|
||||
makeSocketServerUrl(hostName: string, port: number): string {
|
||||
private makeSocketServerUrl(hostName: string, port: number): string {
|
||||
return "ws://" + hostName + ":" + port + "/pipedal";
|
||||
|
||||
}
|
||||
makeVarServerUrl(protocol: string, hostName: string, port: number): string {
|
||||
private makeVarServerUrl(protocol: string, hostName: string, port: number): string {
|
||||
return protocol + "://" + hostName + ":" + port + "/var/";
|
||||
|
||||
}
|
||||
private makeModResourceUrl(protocol: string, hostName: string, port: number): string {
|
||||
return protocol + "://" + hostName + ":" + port + "/resources/";
|
||||
|
||||
}
|
||||
|
||||
async getNextAudioFile(filePath: string): Promise<string> {
|
||||
@@ -1053,6 +1058,7 @@ export class PiPedalModel //implements PiPedalModel
|
||||
if (!socket_server_port) socket_server_port = 8080;
|
||||
let socket_server = this.makeSocketServerUrl(socket_server_address, socket_server_port);
|
||||
let var_server_url = this.makeVarServerUrl("http", socket_server_address, socket_server_port);
|
||||
this.modResourcesUrl = this.makeModResourceUrl("http", socket_server_address, socket_server_port);
|
||||
|
||||
this.socketServerUrl = socket_server;
|
||||
this.varServerUrl = var_server_url;
|
||||
|
||||
@@ -23,6 +23,8 @@ import WithStyles, { withTheme } from './WithStyles';
|
||||
import { createStyles } from './WithStyles';
|
||||
import { css } from '@emotion/react';
|
||||
|
||||
import ModGuiHost, { IModGuiHostSite } from './ModGuiHost';
|
||||
|
||||
import { withStyles } from "tss-react/mui";
|
||||
import { PiPedalModel, PiPedalModelFactory } from './PiPedalModel';
|
||||
import { UiPlugin, UiControl, UiFileProperty, UiFrequencyPlot, ScalePoint } from './Lv2Plugin';
|
||||
@@ -340,6 +342,8 @@ export interface PluginControlViewProps extends WithStyles<typeof styles> {
|
||||
item: PedalboardItem;
|
||||
customization?: ControlViewCustomization;
|
||||
customizationId?: number;
|
||||
showModGui: boolean;
|
||||
onSetShowModGui: (instanceId: number, showModGui: boolean) => void;
|
||||
}
|
||||
type PluginControlViewState = {
|
||||
landscapeGrid: boolean;
|
||||
@@ -360,7 +364,6 @@ const PluginControlView =
|
||||
constructor(props: PluginControlViewProps) {
|
||||
super(props);
|
||||
this.model = PiPedalModelFactory.getInstance();
|
||||
|
||||
this.state = {
|
||||
landscapeGrid: false,
|
||||
imeUiControl: undefined,
|
||||
@@ -376,6 +379,19 @@ const PluginControlView =
|
||||
this.onControlValueChanged = this.onControlValueChanged.bind(this);
|
||||
this.onPreviewChange = this.onPreviewChange.bind(this);
|
||||
}
|
||||
// unmonitorPort: (handle: MonitorPortHandle) => void;
|
||||
// onValueChanged: (instanceId: number, symbol: string, value: number) => void;
|
||||
|
||||
// monitorPatchProperty(
|
||||
// instanceId: number,
|
||||
// propertyUri: string,
|
||||
// onReceived: PatchPropertyListener
|
||||
// ): ListenHandle;
|
||||
|
||||
// cancelMonitorPatchProperty(listenHandle: ListenHandle): void;
|
||||
|
||||
// getPatchProperty(instanceId: number, uri: string): Promise<any>;
|
||||
// setPatchProperty(instanceId: number, uri: string, value: any): Promise<boolean>
|
||||
|
||||
|
||||
onPreviewChange(key: string, value: number): void {
|
||||
@@ -758,19 +774,45 @@ const PluginControlView =
|
||||
}
|
||||
return false;
|
||||
}
|
||||
makeHostSite(): IModGuiHostSite {
|
||||
// Yuck. :-( This worked out badly.
|
||||
let model = this.model;
|
||||
return {
|
||||
monitorPort: this.model.monitorPort.bind(model),
|
||||
unmonitorPort: this.model.unmonitorPort.bind(model),
|
||||
setPedalboardControl: this.model.setPedalboardControl.bind(model),
|
||||
monitorPatchProperty: this.model.monitorPatchProperty.bind(model),
|
||||
cancelMonitorPatchProperty: this.model.cancelMonitorPatchProperty.bind(model),
|
||||
getPatchProperty: this.model.getPatchProperty.bind(model),
|
||||
setPatchProperty: this.model.setPatchProperty.bind(model),
|
||||
};
|
||||
}
|
||||
|
||||
render(): ReactNode {
|
||||
this.controlKeyIndex = 0;
|
||||
const classes = withStyles.getClasses(this.props);
|
||||
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);
|
||||
renderModGui(): ReactNode {
|
||||
let uiPlugin = this.model.getUiPlugin(this.props.item.uri);
|
||||
if (!uiPlugin) {
|
||||
return (<div />);
|
||||
}
|
||||
return (
|
||||
<ModGuiHost
|
||||
instanceId={this.props.instanceId}
|
||||
plugin={uiPlugin}
|
||||
onClose={() => {
|
||||
if (this.props.onSetShowModGui) {
|
||||
this.props.onSetShowModGui(this.props.instanceId, false);
|
||||
}
|
||||
}}
|
||||
hostSite={this.makeHostSite()}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
renderPiPedalControl(pedalboardItem?: PedalboardItem): ReactNode {
|
||||
this.controlKeyIndex = 0;
|
||||
|
||||
|
||||
const classes = withStyles.getClasses(this.props);
|
||||
let pedalboard = this.model.pedalboard.get();
|
||||
|
||||
if (!pedalboardItem)
|
||||
return (<div className={classes.frame} ></div>);
|
||||
@@ -792,19 +834,13 @@ const PluginControlView =
|
||||
|
||||
controlValues = this.filterNotOnGui(controlValues, plugin);
|
||||
|
||||
|
||||
|
||||
let gridClass = this.state.landscapeGrid ? classes.landscapeGrid : classes.normalGrid;
|
||||
let scrollClass = this.state.landscapeGrid ? classes.frameScrollLandscape : classes.frameScrollPortrait;
|
||||
let vuMeterRClass = this.state.landscapeGrid ? classes.vuMeterRLandscape : classes.vuMeterR;
|
||||
let controlNodes: ControlNodes;
|
||||
let frameClass = classes.frame;
|
||||
|
||||
if (this.fullScreen()) {
|
||||
gridClass = classes.noScrollGrid;
|
||||
scrollClass = classes.frameScrollNone;
|
||||
frameClass = classes.noScrollFrame;
|
||||
}
|
||||
let controlNodes: ControlNodes;
|
||||
|
||||
controlNodes = this.getStandardControlNodes(plugin, controlValues);
|
||||
|
||||
@@ -822,16 +858,7 @@ const PluginControlView =
|
||||
if (pedalboardItem.midiChannelBinding) {
|
||||
nodes.push(this.midiBindingControl(pedalboardItem));
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<div className={frameClass}>
|
||||
<div className={classes.vuMeterL}>
|
||||
<VuMeter displayText={true} display="input" instanceId={pedalboardItem.instanceId} />
|
||||
</div>
|
||||
<div className={vuMeterRClass}>
|
||||
<VuMeter displayText={true} display="output" instanceId={pedalboardItem.instanceId} />
|
||||
</div>
|
||||
<div className={scrollClass}>
|
||||
<div className={gridClass} >
|
||||
{
|
||||
@@ -848,6 +875,44 @@ const PluginControlView =
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
|
||||
}
|
||||
render(): ReactNode {
|
||||
const classes = withStyles.getClasses(this.props);
|
||||
let pedalboard = this.model.pedalboard.get();
|
||||
|
||||
let pedalboardItem: PedalboardItem;
|
||||
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);
|
||||
}
|
||||
|
||||
let vuMeterRClass = this.state.landscapeGrid ? classes.vuMeterRLandscape : classes.vuMeterR;
|
||||
let frameClass = classes.frame;
|
||||
|
||||
if (this.fullScreen()) {
|
||||
frameClass = classes.noScrollFrame;
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<div className={frameClass}>
|
||||
<div className={classes.vuMeterL}>
|
||||
<VuMeter displayText={true} display="input" instanceId={pedalboardItem.instanceId} />
|
||||
</div>
|
||||
<div className={vuMeterRClass}>
|
||||
<VuMeter displayText={true} display="output" instanceId={pedalboardItem.instanceId} />
|
||||
</div>
|
||||
{
|
||||
this.props.showModGui && this.renderModGui()
|
||||
|| this.renderPiPedalControl(pedalboardItem)
|
||||
}
|
||||
|
||||
{this.state.showFileDialog && (
|
||||
|
||||
<FilePropertyDialog open={this.state.showFileDialog}
|
||||
@@ -889,7 +954,7 @@ const PluginControlView =
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* xxx: I don't think we need this anyore. */}
|
||||
<FullScreenIME uiControl={this.state.imeUiControl} value={this.state.imeValue}
|
||||
|
||||
onChange={(key, value) => this.onImeValueChange(key, value)}
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
// 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 ResizeResponsiveComponent from './ResizeResponsiveComponent';
|
||||
import { Theme } from '@mui/material/styles';
|
||||
import WithStyles from './WithStyles';
|
||||
import { withStyles } from "tss-react/mui";
|
||||
import {createStyles} from './WithStyles';
|
||||
|
||||
import { PiPedalModel, PiPedalModelFactory } from './PiPedalModel';
|
||||
|
||||
const styles = (theme: Theme) => createStyles({
|
||||
pgraph: {
|
||||
paddingBottom: 16
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
export interface ScratchClassProps extends WithStyles<typeof styles> {
|
||||
}
|
||||
|
||||
export interface ScratchClassState {
|
||||
screenWidth: number,
|
||||
screenHeight: number
|
||||
}
|
||||
|
||||
|
||||
|
||||
const ScratchClass = withStyles(
|
||||
class extends ResizeResponsiveComponent<ScratchClassProps, ScratchClassState> {
|
||||
private model: PiPedalModel;
|
||||
constructor(props: ScratchClassProps) {
|
||||
super(props);
|
||||
|
||||
this.model = PiPedalModelFactory.getInstance();
|
||||
void this.model; // suppress unused variable warning
|
||||
|
||||
this.state = {
|
||||
screenWidth: this.windowSize.width,
|
||||
screenHeight: this.windowSize.height
|
||||
};
|
||||
}
|
||||
mounted: boolean = false;
|
||||
|
||||
|
||||
|
||||
onWindowSizeChanged(width: number, height: number): void {
|
||||
this.setState({ screenWidth: width, screenHeight: height });
|
||||
}
|
||||
|
||||
|
||||
componentDidMount() {
|
||||
super.componentDidMount();
|
||||
this.mounted = true;
|
||||
}
|
||||
componentWillUnmount() {
|
||||
this.mounted = false;
|
||||
super.componentWillUnmount();
|
||||
}
|
||||
|
||||
|
||||
componentDidUpdate(prevProps: ScratchClassProps) {
|
||||
}
|
||||
|
||||
|
||||
|
||||
render() {
|
||||
const classes = withStyles.getClasses(this.props);
|
||||
void classes; // suppress unused variable warning
|
||||
return (<div/>);
|
||||
}
|
||||
},
|
||||
styles);
|
||||
|
||||
export default ScratchClass;
|
||||
@@ -0,0 +1,25 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 6.3499999 6.35"
|
||||
version="1.1"
|
||||
id="svg5"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<defs
|
||||
id="defs2" />
|
||||
<g
|
||||
id="layer1">
|
||||
<path
|
||||
style="fill-rule:evenodd;-inkscape-stroke:none"
|
||||
d="m 2.2089844,0.26367188 c -0.564979,0 -1.0234375,0.46978477 -1.0234375,1.03710932 v 3.7578125 c 0,0.5673246 0.4584585,1.0371094 1.0234375,1.0371094 H 4.140625 c 0.564979,0 1.0234375,-0.4697848 1.0234375,-1.0371094 V 1.3007812 c 0,-0.56732455 -0.4584585,-1.03710932 -1.0234375,-1.03710932 z m 0,0.52929687 H 4.140625 c 0.276179,0 0.4941406,0.21977775 0.4941406,0.50781245 v 3.7578125 c 0,0.2880348 -0.2179616,0.5078125 -0.4941406,0.5078125 H 2.2089844 c -0.276179,0 -0.4941406,-0.2197777 -0.4941407,-0.5078125 V 1.3007812 c 0,-0.2880347 0.2179617,-0.50781245 0.4941407,-0.50781245 z"
|
||||
id="rect234" />
|
||||
<path
|
||||
style="-inkscape-stroke:none"
|
||||
d="m 3.1757813,3.0429688 c -0.4352418,0 -0.7949219,0.357727 -0.7949219,0.7929687 0,0.4352417 0.3596801,0.7949219 0.7949219,0.7949219 0.4352417,0 0.7929687,-0.3596802 0.7929687,-0.7949219 0,-0.4352417 -0.357727,-0.7929687 -0.7929687,-0.7929687 z m 0,0.5292968 c 0.1492592,0 0.2636718,0.1144129 0.2636718,0.2636719 0,0.149259 -0.1144126,0.265625 -0.2636718,0.265625 -0.1492593,0 -0.2656251,-0.116366 -0.265625,-0.265625 0,-0.149259 0.1163657,-0.2636719 0.265625,-0.2636719 z"
|
||||
id="path2914" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
@@ -0,0 +1,23 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 6.3499999 6.35"
|
||||
version="1.1"
|
||||
id="svg5"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<defs
|
||||
id="defs2" />
|
||||
<g
|
||||
id="layer1">
|
||||
<path
|
||||
id="path3692"
|
||||
d="M 1.7218342,0.53148711 C 1.03731,0.54023357 0.48004958,1.102787 0.48004958,1.7892914 c 0,0.69191 0.56589442,1.2578044 1.25780432,1.2578044 0.69191,0 1.2598715,-0.5658944 1.2598715,-1.2578044 0,-0.5024751 -0.2996529,-0.9383787 -0.7286377,-1.13946526 L 2.1460978,1.1831269 c 0.1950392,0.1300488 0.3224609,0.3518001 0.3224609,0.6061645 0,0.4059278 -0.324777,0.7286377 -0.7307048,0.7286377 -0.4059278,0 -0.7286376,-0.3227099 -0.7286376,-0.7286377 0,-0.3691252 0.2670204,-0.669466 0.6206339,-0.7208862 L 1.4810221,1.7143606 1.7388875,1.7737885 1.8990844,1.0782237 2.0174234,0.56300973 c -0.0053,-0.001214 -0.010694,-0.002471 -0.01602,-0.003617 -0.00604,-0.001301 -0.012021,-0.002404 -0.018087,-0.003617 -0.00465,-9.2974e-4 -0.00928,-0.001706 -0.013953,-0.002584 -0.0066,-0.001242 -0.013523,-0.002479 -0.020154,-0.003617 -0.00411,-7.0267e-4 -0.00828,-0.001405 -0.012402,-0.002067 -0.00736,-0.001187 -0.01482,-0.00256 -0.022221,-0.003617 -8.711e-4,-1.2398e-4 -0.00171,-3.9459e-4 -0.00258,-5.1676e-4 -0.010646,-0.001498 -0.021313,-0.002391 -0.032039,-0.003617 -0.00292,-3.3196e-4 -0.00586,-7.218e-4 -0.00879,-0.001034 -0.00879,-9.4149e-4 -0.017517,-0.001827 -0.026355,-0.002584 -0.00221,-1.8873e-4 -0.0045,-3.3967e-4 -0.00672,-5.1677e-4 -0.00954,-7.6485e-4 -0.019336,-0.00152 -0.028939,-0.002067 -0.00717,-4.0808e-4 -0.014505,-7.4831e-4 -0.021704,-0.001034 -0.00474,-1.8774e-4 -0.0092,-3.827e-4 -0.013953,-5.1677e-4 -0.00657,-1.8642e-4 -0.013046,-4.3271e-4 -0.019637,-5.1676e-4 -0.00537,-6.855e-5 -0.010629,0 -0.01602,0 -0.00541,0 -0.01063,-6.887e-5 -0.01602,0 z" />
|
||||
<path
|
||||
id="path3861"
|
||||
d="m 4.23951,2.741472 c -0.6845242,0.00875 -1.2417846,0.5712999 -1.2417846,1.2578043 0,0.69191 0.5658944,1.2578044 1.2578043,1.2578044 0.69191,0 1.2598715,-0.5658944 1.2598715,-1.2578044 0,-0.5024751 -0.2996529,-0.9383787 -0.7286377,-1.1394653 L 4.6637736,3.3931118 c 0.1950392,0.1300488 0.3224609,0.3518001 0.3224609,0.6061645 0,0.4059278 -0.324777,0.7286377 -0.7307048,0.7286377 -0.4059278,0 -0.7286377,-0.3227099 -0.7286377,-0.7286377 0,-0.3691252 0.2670204,-0.669466 0.620634,-0.7208862 l -0.1488281,0.6459554 0.2578654,0.059428 0.1601969,-0.6955648 0.118339,-0.515214 c -0.0053,-0.00121 -0.010694,-0.00247 -0.01602,-0.00362 -0.00604,-0.0013 -0.012021,-0.0024 -0.018087,-0.00362 -0.00465,-9.297e-4 -0.00928,-0.00171 -0.013953,-0.00258 -0.0066,-0.00124 -0.013523,-0.00248 -0.020154,-0.00362 -0.00411,-7.027e-4 -0.00828,-0.0014 -0.012402,-0.00207 -0.00736,-0.00119 -0.01482,-0.00256 -0.022221,-0.00362 -8.711e-4,-1.24e-4 -0.00171,-3.946e-4 -0.00258,-5.168e-4 -0.010646,-0.0015 -0.021313,-0.00239 -0.032039,-0.00362 -0.00292,-3.319e-4 -0.00586,-7.218e-4 -0.00878,-0.00103 -0.00879,-9.415e-4 -0.017517,-0.00183 -0.026355,-0.00258 -0.00221,-1.887e-4 -0.0045,-3.397e-4 -0.00672,-5.168e-4 -0.00954,-7.648e-4 -0.019336,-0.00152 -0.028939,-0.00207 -0.00717,-4.081e-4 -0.014505,-7.483e-4 -0.021704,-0.00103 -0.00474,-1.877e-4 -0.0092,-3.827e-4 -0.013953,-5.167e-4 -0.00657,-1.864e-4 -0.013046,-4.327e-4 -0.019637,-5.168e-4 -0.00537,-6.85e-5 -0.010629,0 -0.01602,0 -0.00541,0 -0.01063,-6.89e-5 -0.01602,0 z" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.4 KiB |
@@ -8,4 +8,12 @@ export default defineConfig({
|
||||
chunkSizeWarningLimit: 2000
|
||||
},
|
||||
plugins: [react(),svgr()],
|
||||
server: {
|
||||
proxy: {
|
||||
'/resources': {
|
||||
target: 'http://localhost:8080',
|
||||
changeOrigin: false,
|
||||
},
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||