Toob File Player checkpoint.
This commit is contained in:
@@ -14,6 +14,29 @@
|
||||
}
|
||||
div {
|
||||
min-width: 0;
|
||||
background: "red";
|
||||
}
|
||||
|
||||
input[type="number"].scrollMod::-webkit-outer-spin-button,
|
||||
input[type="number"].scrollMod::-webkit-inner-spin-button {
|
||||
-webkit-appearance: none;
|
||||
color: #FFF;
|
||||
background: #0001 url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAJCAYAAADgkQYQAAAAKUlEQVQYlWNgwAT/sYhhKPiPT+F/LJgEsHv37v+EMGkmkuImoh2NoQAANlcun/q4OoYAAAAASUVORK5CYII=) no-repeat center center;
|
||||
filter: invert(100%);
|
||||
width: 1em;
|
||||
padding: 2px;
|
||||
opacity: .6; /* shows Spin Buttons per default (Chrome >= 39) */
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
input[type="number"].scrollMod::-webkit-inner-spin-button:hover,
|
||||
input[type="number"].scrollMod::-webkit-inner-spin-button:active {
|
||||
background: #0003 url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAJCAYAAADgkQYQAAAAKUlEQVQYlWNgwAT/sYhhKPiPT+F/LJgEsHv37v+EMGkmkuImoh2NoQAANlcun/q4OoYAAAAASUVORK5CYII=) no-repeat center center;
|
||||
opacity: .8;
|
||||
}
|
||||
|
||||
input[type=number] {
|
||||
-moz-appearance: textfield;
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
|
||||
export default class AudioFileMetadata {
|
||||
//AudioFileMetadata&operator=(const AudioFileMetadata&) = default;
|
||||
deserialize(o: any) {
|
||||
this.title = o.title;
|
||||
this.duration = o.duration;
|
||||
this.track = o.track;
|
||||
this.album = o.album;
|
||||
this.disc = o.disc;
|
||||
this.artist = o.artist;
|
||||
this.albumArtist = o.albumArtist;
|
||||
return this;
|
||||
}
|
||||
duration: number = 0;
|
||||
title: string = "";
|
||||
track: string = "";
|
||||
album: string = "";
|
||||
disc: string = "";
|
||||
artist: string = "";
|
||||
albumArtist: string = "";
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* 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 { Typography } from "@mui/material";
|
||||
import Tooltip from "@mui/material/Tooltip";
|
||||
import { ReactElement } from "react";
|
||||
|
||||
|
||||
export interface ButtonTooltipProps {
|
||||
title: string,
|
||||
children: ReactElement
|
||||
}
|
||||
|
||||
export default function ButtonTooltip(props: ButtonTooltipProps)
|
||||
{
|
||||
return (
|
||||
<Tooltip placement="top-start" arrow enterDelay={1500} enterNextDelay={1500}
|
||||
title={
|
||||
(
|
||||
<Typography variant="body2">
|
||||
{props.title}
|
||||
</Typography>
|
||||
)
|
||||
}
|
||||
>
|
||||
{props.children}
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
@@ -30,13 +30,15 @@ import { GxTunerViewFactory } from './GxTunerView';
|
||||
import ToobPowerstage2ViewFactory from './ToobPowerStage2View';
|
||||
import ToobSpectrumAnalyzerViewFactory from './ToobSpectrumAnalyzerView';
|
||||
import ToobMLViewFactory from './ToobMLView';
|
||||
import ToobPlayerFactory from './ToobPlayerView';
|
||||
|
||||
|
||||
let pluginFactories: IControlViewFactory[] = [
|
||||
new GxTunerViewFactory(),
|
||||
new ToobPowerstage2ViewFactory(),
|
||||
new ToobSpectrumAnalyzerViewFactory(),
|
||||
new ToobMLViewFactory()
|
||||
new ToobMLViewFactory(),
|
||||
new ToobPlayerFactory()
|
||||
];
|
||||
|
||||
|
||||
|
||||
@@ -481,9 +481,6 @@ export default withStyles(
|
||||
|
||||
}
|
||||
renderBreadcrumbs() {
|
||||
if (this.state.navDirectory === "") {
|
||||
return (<Divider />);
|
||||
}
|
||||
let breadcrumbs: React.ReactElement[] = [(
|
||||
<Button variant="text"
|
||||
color="inherit"
|
||||
|
||||
@@ -77,7 +77,11 @@ const GxTunerView =
|
||||
}
|
||||
throw new Error("GxTuner: Control '" + key + "' not found.");
|
||||
}
|
||||
ModifyControls(controls: (React.ReactNode| ControlGroup)[]): (React.ReactNode| ControlGroup)[]
|
||||
fullScreen() {
|
||||
return false;
|
||||
}
|
||||
|
||||
modifyControls(controls: (React.ReactNode| ControlGroup)[]): (React.ReactNode| ControlGroup)[]
|
||||
{
|
||||
let refFreqIndex = this.getControlIndex("REFFREQ");
|
||||
let thresholdIndex = this.getControlIndex("THRESHOLD");
|
||||
|
||||
@@ -93,7 +93,7 @@ const styles = ({ palette }: Theme) => { return {
|
||||
flex: "0 0 64px", width: "100%", paddingLeft: 24, paddingRight: 16, paddingBottom: 16
|
||||
}),
|
||||
controlContent: css({
|
||||
flex: "1 1 auto", width: "100%", overflowY: "auto", minHeight: 240
|
||||
flex: "1 1 auto", width: "100%", overflowY: "hidden", minHeight: 300
|
||||
}),
|
||||
controlContentSmall: css({
|
||||
flex: "0 0 162px", width: "100%", height: 162, overflowY: "hidden",
|
||||
@@ -609,7 +609,7 @@ export const MainPage =
|
||||
</div>
|
||||
)
|
||||
}
|
||||
<div className={horizontalScrollLayout ? classes.controlContentSmall : classes.controlContent}>
|
||||
<div id="mainPageControls" className={horizontalScrollLayout ? classes.controlContentSmall : classes.controlContent}>
|
||||
{
|
||||
missing ? (
|
||||
<div style={{ marginLeft: 40, marginTop: 20 }}>
|
||||
|
||||
@@ -41,6 +41,7 @@ import AlsaDeviceInfo from './AlsaDeviceInfo';
|
||||
import { AndroidHostInterface, FakeAndroidHost } from './AndroidHost';
|
||||
import { ColorTheme, getColorScheme, setColorScheme } from './DarkMode';
|
||||
import FilePropertyDirectoryTree from './FilePropertyDirectoryTree';
|
||||
import AudioFileMetadata from './AudioFileMetadaa';
|
||||
|
||||
|
||||
export enum State {
|
||||
@@ -56,14 +57,11 @@ export enum State {
|
||||
HotspotChanging,
|
||||
};
|
||||
|
||||
function getErrorMessage(error: any)
|
||||
{
|
||||
if (error instanceof Error)
|
||||
{
|
||||
function getErrorMessage(error: any) {
|
||||
if (error instanceof Error) {
|
||||
return (error as Error).message;
|
||||
}
|
||||
if (!error)
|
||||
{
|
||||
if (!error) {
|
||||
return "";
|
||||
}
|
||||
return error.toString();
|
||||
@@ -479,14 +477,12 @@ export class PiPedalModel //implements PiPedalModel
|
||||
|
||||
private expectDisconnectTimer?: number = undefined;
|
||||
|
||||
cancelExpectDisconnectTimer()
|
||||
{
|
||||
cancelExpectDisconnectTimer() {
|
||||
if (this.expectDisconnectTimer) {
|
||||
clearTimeout(this.expectDisconnectTimer);
|
||||
}
|
||||
}
|
||||
startExpectDisconnectTimer()
|
||||
{
|
||||
startExpectDisconnectTimer() {
|
||||
this.cancelExpectDisconnectTimer();
|
||||
// poll for access to a running pipedal server
|
||||
this.expectDisconnectTimer = setTimeout(
|
||||
@@ -500,8 +496,7 @@ export class PiPedalModel //implements PiPedalModel
|
||||
expectDisconnect(reason: ReconnectReason) {
|
||||
this.cancelExpectDisconnectTimer();
|
||||
this.reconnectReason = reason;
|
||||
if (this.reconnectReason !== ReconnectReason.Disconnected)
|
||||
{
|
||||
if (this.reconnectReason !== ReconnectReason.Disconnected) {
|
||||
this.startExpectDisconnectTimer();
|
||||
}
|
||||
}
|
||||
@@ -619,17 +614,14 @@ export class PiPedalModel //implements PiPedalModel
|
||||
let channelSelection = new JackChannelSelection().deserialize(channelSelectionBody.jackChannelSelection);
|
||||
this.jackSettings.set(channelSelection);
|
||||
} else if (message === "onSnapshotModified") {
|
||||
let {snapshotIndex,modified} = (body as {snapshotIndex: number, modified: boolean});
|
||||
let { snapshotIndex, modified } = (body as { snapshotIndex: number, modified: boolean });
|
||||
let snapshots = this.pedalboard.get().snapshots;
|
||||
if (snapshotIndex >= 0 && snapshotIndex < snapshots.length)
|
||||
{
|
||||
if (snapshotIndex >= 0 && snapshotIndex < snapshots.length) {
|
||||
let snapshot = snapshots[snapshotIndex]
|
||||
if (snapshot)
|
||||
{
|
||||
if (snapshot.isModified !== modified)
|
||||
{
|
||||
if (snapshot) {
|
||||
if (snapshot.isModified !== modified) {
|
||||
snapshot.isModified = modified;
|
||||
this.onSnapshotModified.fire({snapshotIndex: snapshotIndex,modified: modified});
|
||||
this.onSnapshotModified.fire({ snapshotIndex: snapshotIndex, modified: modified });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -691,8 +683,8 @@ export class PiPedalModel //implements PiPedalModel
|
||||
} else if (message === "onNotifyPathPatchPropertyChanged") {
|
||||
let instanceId = body.instanceId as number;
|
||||
let propertyUri = body.propertyUri as string;
|
||||
let atomJson = body.atomJson as any;
|
||||
this.handleNotifyPathPatchPropertyChanged(instanceId, propertyUri, atomJson);
|
||||
let atomJsonString = body.atomJson as string;
|
||||
this.handleNotifyPathPatchPropertyChanged(instanceId, propertyUri, atomJsonString);
|
||||
if (header.replyTo) {
|
||||
this.webSocket?.reply(header.replyTo, "onNotifyPathPatchPropertyChanged", true);
|
||||
}
|
||||
@@ -752,7 +744,7 @@ export class PiPedalModel //implements PiPedalModel
|
||||
else if (message === "onHasWifiChanged") {
|
||||
let hasWifi = body as boolean;
|
||||
this.hasWifiDevice.set(hasWifi);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -923,6 +915,8 @@ export class PiPedalModel //implements PiPedalModel
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async onSocketReconnected(): Promise<void> {
|
||||
this.cancelOnNetworkChanging();
|
||||
this.cancelAndroidReconnectTimer();
|
||||
@@ -932,13 +926,12 @@ export class PiPedalModel //implements PiPedalModel
|
||||
}
|
||||
|
||||
if (this.visibilityState.get() === VisibilityState.Hidden) return;
|
||||
|
||||
|
||||
// reload state, but not configuration.
|
||||
this.clientId = await this.getWebSocket().request<number>("hello");
|
||||
|
||||
let newServerVersion = this.serverVersion = await this.getWebSocket().request<PiPedalVersion>("version");
|
||||
if (newServerVersion.serverVersion !== this.serverVersion.serverVersion)
|
||||
{
|
||||
let newServerVersion = this.serverVersion = await this.getWebSocket().request<PiPedalVersion>("version");
|
||||
if (newServerVersion.serverVersion !== this.serverVersion.serverVersion) {
|
||||
this.reloadPage();
|
||||
return;
|
||||
}
|
||||
@@ -955,6 +948,48 @@ export class PiPedalModel //implements PiPedalModel
|
||||
|
||||
}
|
||||
|
||||
async getNextAudioFile(filePath: string): Promise<string> {
|
||||
try {
|
||||
let url =
|
||||
this.varServerUrl
|
||||
+ "NextAudioFile?path=" + encodeURIComponent(filePath);
|
||||
|
||||
let response = await fetch(url);
|
||||
let json = await response.json();
|
||||
return json as string;
|
||||
} catch (e) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
async getPreviousAudioFile(filePath: string): Promise<string> {
|
||||
try {
|
||||
let url =
|
||||
this.varServerUrl
|
||||
+ "PreviousAudioFile?path=" + encodeURIComponent(filePath);
|
||||
|
||||
let response = await fetch(url);
|
||||
let json = await response.json();
|
||||
return json as string;
|
||||
} catch (e) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
async getAudioFileMetadata(filePath: string): Promise<AudioFileMetadata> {
|
||||
try {
|
||||
let url =
|
||||
this.varServerUrl
|
||||
+ "AudioMetadata?path=" + encodeURIComponent(filePath);
|
||||
|
||||
let response = await fetch(url);
|
||||
let json = await response.json();
|
||||
return new AudioFileMetadata().deserialize(json);
|
||||
} catch (e) {
|
||||
return new AudioFileMetadata();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
maxFileUploadSize: number = 512 * 1024 * 1024;
|
||||
maxPresetUploadSize: number = 1024 * 1024;
|
||||
debug: boolean = false;
|
||||
@@ -964,7 +999,7 @@ export class PiPedalModel //implements PiPedalModel
|
||||
async requestConfig(): Promise<boolean> {
|
||||
|
||||
try {
|
||||
const myRequest = new Request(this.varRequest('config.json'));
|
||||
const myRequest = new Request(this.varRequest('config.json'));
|
||||
let response: Response = await fetch(myRequest);
|
||||
let data = await response.json();
|
||||
|
||||
@@ -984,12 +1019,11 @@ 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.socketServerUrl = socket_server;
|
||||
this.varServerUrl = var_server_url;
|
||||
this.maxFileUploadSize = parseInt(max_upload_size);
|
||||
} catch (error: any)
|
||||
{
|
||||
} catch (error: any) {
|
||||
this.setError("Can't connect to server. " + getErrorMessage(error));
|
||||
return false;
|
||||
}
|
||||
@@ -1005,10 +1039,10 @@ export class PiPedalModel //implements PiPedalModel
|
||||
}
|
||||
);
|
||||
|
||||
try {
|
||||
try {
|
||||
await this.webSocket.connect();
|
||||
} catch (error) {
|
||||
this.setError("Failed to connect to server. " + getErrorMessage(error) );
|
||||
this.setError("Failed to connect to server. " + getErrorMessage(error));
|
||||
return false;
|
||||
|
||||
}
|
||||
@@ -1017,23 +1051,21 @@ export class PiPedalModel //implements PiPedalModel
|
||||
|
||||
this.clientId = (await this.getWebSocket().request<number>("hello")) as number;
|
||||
|
||||
this.preloadImages( (await this.getWebSocket().request<string>("imageList")));
|
||||
this.preloadImages((await this.getWebSocket().request<string>("imageList")));
|
||||
} catch (error) {
|
||||
this.setError("Failed to establish connection. " + getErrorMessage(error));
|
||||
return false;
|
||||
}
|
||||
return await this.loadServerState();
|
||||
}
|
||||
async loadServerState() : Promise<boolean>
|
||||
{
|
||||
try
|
||||
{
|
||||
async loadServerState(): Promise<boolean> {
|
||||
try {
|
||||
this.serverVersion = await this.getWebSocket().request<PiPedalVersion>("version");
|
||||
|
||||
this.updateStatus.set(new UpdateStatus().deserialize(await this.getUpdateStatus()));
|
||||
|
||||
this.hasWifiDevice.set(await this.getWebSocket().request<boolean>("getHasWifi"));
|
||||
|
||||
|
||||
this.ui_plugins.set(
|
||||
UiPlugin.deserialize_array(await this.getWebSocket().request<any>("plugins"))
|
||||
);
|
||||
@@ -1059,7 +1091,7 @@ export class PiPedalModel //implements PiPedalModel
|
||||
this.wifiDirectConfigSettings.set(
|
||||
new WifiDirectConfigSettings().deserialize(
|
||||
await this.getWebSocket().request<any>("getWifiDirectConfigSettings")
|
||||
));
|
||||
));
|
||||
this.governorSettings.set(new GovernorSettings().deserialize(
|
||||
await this.getWebSocket().request<any>("getGovernorSettings")
|
||||
));
|
||||
@@ -1077,7 +1109,7 @@ export class PiPedalModel //implements PiPedalModel
|
||||
this.jackSettings.set(new JackChannelSelection().deserialize(
|
||||
await this.getWebSocket().request<any>("getJackSettings")
|
||||
));
|
||||
this.banks.set(new BankIndex().deserialize(await this.getWebSocket().request<any>("getBankIndex")));
|
||||
this.banks.set(new BankIndex().deserialize(await this.getWebSocket().request<any>("getBankIndex")));
|
||||
|
||||
this.favorites.set(await this.getWebSocket().request<FavoritesList>("getFavorites"));
|
||||
|
||||
@@ -1089,7 +1121,7 @@ export class PiPedalModel //implements PiPedalModel
|
||||
this.setState(State.Ready);
|
||||
return true;
|
||||
}
|
||||
catch(error) {
|
||||
catch (error) {
|
||||
this.setError("Failed to fetch server state.\n\n" + getErrorMessage(error));
|
||||
return false;
|
||||
}
|
||||
@@ -1473,7 +1505,7 @@ export class PiPedalModel //implements PiPedalModel
|
||||
}
|
||||
|
||||
|
||||
sendPedalboardControlTrigger(instanceId: number, key: string, value: number) : void {
|
||||
sendPedalboardControlTrigger(instanceId: number, key: string, value: number): void {
|
||||
// no state change, no saving the value, just send it to the realtime thread/
|
||||
this._setServerControl("previewControl", instanceId, key, value);
|
||||
}
|
||||
@@ -2334,19 +2366,15 @@ export class PiPedalModel //implements PiPedalModel
|
||||
}
|
||||
|
||||
private handleNotifyPathPatchPropertyChanged(
|
||||
instanceId: number, propertyUri: string, jsonObject: any
|
||||
instanceId: number, propertyUri: string, jsonObjectString: string
|
||||
) {
|
||||
let pedalboard = this.pedalboard.get();
|
||||
let pedalboardItem = pedalboard.getItem(instanceId);
|
||||
if (pedalboardItem) {
|
||||
pedalboardItem.pathProperties[propertyUri] = jsonObject;
|
||||
}
|
||||
for (let i = 0; i < this.monitorPatchPropertyListeners.length; ++i) {
|
||||
let listener = this.monitorPatchPropertyListeners[i];
|
||||
if (listener.instanceId === instanceId) {
|
||||
listener.callback(instanceId, propertyUri, jsonObject);
|
||||
}
|
||||
pedalboardItem.pathProperties[propertyUri] = jsonObjectString;
|
||||
}
|
||||
// No NOT notify monitorPatchProperty listeners, because they extpect objects not strings,
|
||||
// AND they will a NotifyPatchPropertychanged message anyway.
|
||||
|
||||
}
|
||||
private handleNotifyPatchProperty(clientHandle: number, instanceId: number, propertyUri: string, jsonObject: any) {
|
||||
@@ -2451,16 +2479,14 @@ export class PiPedalModel //implements PiPedalModel
|
||||
}
|
||||
})
|
||||
.then((json) => {
|
||||
let response = json as {errorMessage: string, path: string};
|
||||
if (response.errorMessage !== "")
|
||||
{
|
||||
let response = json as { errorMessage: string, path: string };
|
||||
if (response.errorMessage !== "") {
|
||||
throw new Error(response.errorMessage);
|
||||
}
|
||||
resolve(response.path);
|
||||
})
|
||||
.catch((error) => {
|
||||
if (error instanceof Error)
|
||||
{
|
||||
if (error instanceof Error) {
|
||||
reject("Upload failed. " + (error as Error).message);
|
||||
} else {
|
||||
reject("Upload failed. " + error);
|
||||
@@ -2605,7 +2631,7 @@ export class PiPedalModel //implements PiPedalModel
|
||||
});
|
||||
}
|
||||
|
||||
getFilePropertyDirectoryTree(uiFileProperty: UiFileProperty,selectedPath: string): Promise<FilePropertyDirectoryTree> {
|
||||
getFilePropertyDirectoryTree(uiFileProperty: UiFileProperty, selectedPath: string): Promise<FilePropertyDirectoryTree> {
|
||||
return new Promise<FilePropertyDirectoryTree>((resolve, reject) => {
|
||||
let ws = this.webSocket;
|
||||
if (!ws) {
|
||||
@@ -2614,7 +2640,7 @@ export class PiPedalModel //implements PiPedalModel
|
||||
}
|
||||
ws.request<FilePropertyDirectoryTree>(
|
||||
"getFilePropertyDirectoryTree",
|
||||
{fileProperty: uiFileProperty, selectedPath: selectedPath}
|
||||
{ fileProperty: uiFileProperty, selectedPath: selectedPath }
|
||||
).then((result) => {
|
||||
resolve(new FilePropertyDirectoryTree().deserialize(result));
|
||||
}).catch((e) => {
|
||||
|
||||
@@ -65,14 +65,14 @@ function preventNextClickAfterDrag() {
|
||||
// on ANY element under the mouse. Prevent this click event
|
||||
// (and any other click event) from happening for 100ms
|
||||
// after the drag stops.
|
||||
let clickHandler = (e: MouseEvent) => {
|
||||
let clickHandler = (e: MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
};
|
||||
document.addEventListener('click',clickHandler,true);
|
||||
document.addEventListener('click', clickHandler, true);
|
||||
window.setTimeout(() => {
|
||||
document.removeEventListener('click',clickHandler,true);
|
||||
},100);
|
||||
document.removeEventListener('click', clickHandler, true);
|
||||
}, 100);
|
||||
}
|
||||
|
||||
function androidEmoji(text: string) {
|
||||
@@ -119,7 +119,7 @@ export const pluginControlStyles = (theme: Theme) => createStyles({
|
||||
right: 0,
|
||||
bottom: 4,
|
||||
textAlign: "center",
|
||||
background: theme.mainBackground,
|
||||
background: "transparent",
|
||||
color: theme.palette.text.secondary,
|
||||
// zIndex: -1,
|
||||
}),
|
||||
@@ -214,19 +214,19 @@ const PluginControl =
|
||||
inputChanged: boolean = false;
|
||||
|
||||
onInputLostFocus(event: any): void {
|
||||
this.setState({editFocused: false});
|
||||
this.setState({ editFocused: false });
|
||||
if (this.inputChanged) // validation requried?
|
||||
{
|
||||
this.inputChanged = false;
|
||||
this.validateInput(event, true);
|
||||
|
||||
}
|
||||
|
||||
|
||||
//this.displayValueRef.current!.style.display = "block";
|
||||
}
|
||||
onInputFocus(event: SyntheticEvent): void {
|
||||
//this.displayValueRef.current!.style.display = "none";
|
||||
this.setState({editFocused: true});
|
||||
this.setState({ editFocused: true });
|
||||
if (Utility.hasIMEKeyboard()) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
@@ -356,11 +356,9 @@ const PluginControl =
|
||||
if (!this.mouseDown && this.isValidPointer(e)) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (document.activeElement)
|
||||
{
|
||||
if (document.activeElement) {
|
||||
let e = document.activeElement as any;
|
||||
if (e.blur)
|
||||
{
|
||||
if (e.blur) {
|
||||
e.blur();
|
||||
}
|
||||
}
|
||||
@@ -492,8 +490,7 @@ const PluginControl =
|
||||
onPointerUp(e: PointerEvent<SVGSVGElement>) {
|
||||
|
||||
if (this.isCapturedPointer(e)) {
|
||||
if (this.pointersDown !== 0)
|
||||
{
|
||||
if (this.pointersDown !== 0) {
|
||||
--this.pointersDown;
|
||||
}
|
||||
|
||||
@@ -515,8 +512,7 @@ const PluginControl =
|
||||
preventNextClickAfterDrag();
|
||||
|
||||
} else {
|
||||
if (this.pointersDown !== 0)
|
||||
{
|
||||
if (this.pointersDown !== 0) {
|
||||
--this.pointersDown;
|
||||
}
|
||||
|
||||
@@ -620,7 +616,7 @@ const PluginControl =
|
||||
UpdateGraphicEqPath(imgElement, range);
|
||||
}
|
||||
} else {
|
||||
|
||||
|
||||
let transform = this.rangeToRotationTransform(range);
|
||||
if (this.mouseDown && !commitValue) {
|
||||
transform += " scale(1.5, 1.5)";
|
||||
@@ -1068,21 +1064,30 @@ const PluginControl =
|
||||
defaultValue={control.formatShortValue(value)}
|
||||
error={this.state.error}
|
||||
inputProps={{
|
||||
className: "scrollMod",
|
||||
min: this.props.uiControl?.min_value,
|
||||
max: this.props.uiControl?.max_value,
|
||||
'aria-label':
|
||||
control.symbol + " value",
|
||||
style: { textAlign: "center", fontSize: FONT_SIZE },
|
||||
}}
|
||||
inputRef={this.inputRef} onChange={this.onInputChange}
|
||||
onBlur={this.onInputLostFocus}
|
||||
onFocus={this.onInputFocus}
|
||||
|
||||
onKeyPress={this.onInputKeyPress} />
|
||||
<div className={classes.displayValue}
|
||||
ref={this.displayValueRef} onClick={(e) => { this.inputRef.current!.focus(); }}
|
||||
style={{display: this.state.editFocused? "none": "block"}}
|
||||
>
|
||||
sx={{
|
||||
// Style the input element
|
||||
'& input[type=number]': {
|
||||
width: 60,
|
||||
opacity: this.state.editFocused ? 1 : 0,
|
||||
textAlign: "center", fontSize: FONT_SIZE,
|
||||
borderBottom: "0px",
|
||||
},
|
||||
}}
|
||||
|
||||
inputRef={this.inputRef} onChange={this.onInputChange}
|
||||
onBlur={this.onInputLostFocus}
|
||||
onFocus={this.onInputFocus}
|
||||
|
||||
onKeyPress={this.onInputKeyPress} />
|
||||
<div className={classes.displayValue}
|
||||
ref={this.displayValueRef} onClick={(e) => { this.inputRef.current!.focus(); }}
|
||||
style={{ display: this.state.editFocused ? "none" : "block" }}
|
||||
>
|
||||
<Typography noWrap color="inherit" style={{ fontSize: "12.8px", paddingTop: 4, paddingBottom: 6 }}
|
||||
|
||||
>
|
||||
|
||||
@@ -19,8 +19,8 @@
|
||||
|
||||
import { ReactNode } from 'react';
|
||||
import { Theme } from '@mui/material/styles';
|
||||
import WithStyles, {withTheme} from './WithStyles';
|
||||
import {createStyles} from './WithStyles';
|
||||
import WithStyles, { withTheme } from './WithStyles';
|
||||
import { createStyles } from './WithStyles';
|
||||
import { css } from '@emotion/react';
|
||||
|
||||
import { withStyles } from "tss-react/mui";
|
||||
@@ -97,10 +97,37 @@ const styles = (theme: Theme) => createStyles({
|
||||
overflowX: "hidden",
|
||||
overflowY: "hidden"
|
||||
}),
|
||||
noScrollFrame: css({
|
||||
display: "block",
|
||||
position: "relative",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
|
||||
left: 0, top: 0,
|
||||
flexDirection: "row",
|
||||
flexWrap: "nowrap",
|
||||
paddingTop: "0px",
|
||||
paddingBottom: "0px",
|
||||
overflowX: "hidden",
|
||||
overflowY: "hidden"
|
||||
|
||||
}),
|
||||
frameScrollNone: css({
|
||||
display: "block",
|
||||
position: "relative",
|
||||
left: 0, top: 0,
|
||||
width: "100%", height: "100%",
|
||||
flexDirection: "row",
|
||||
flexWrap: "nowrap",
|
||||
paddingTop: "0px",
|
||||
paddingBottom: "0px",
|
||||
overflowX: "auto",
|
||||
overflowY: "hidden"
|
||||
}),
|
||||
frameScrollLandscape: css({
|
||||
display: "block",
|
||||
position: "absolute",
|
||||
left: 0, top: 0, right: 0, bottom:0,
|
||||
left: 0, top: 0, right: 0, bottom: 0,
|
||||
flexDirection: "row",
|
||||
flexWrap: "nowrap",
|
||||
paddingTop: "0px",
|
||||
@@ -111,7 +138,7 @@ const styles = (theme: Theme) => createStyles({
|
||||
frameScrollPortrait: css({
|
||||
display: "block",
|
||||
position: "absolute",
|
||||
left: 0, top: 0, right: 0, bottom:0,
|
||||
left: 0, top: 0, right: 0, bottom: 0,
|
||||
flexDirection: "row",
|
||||
flexWrap: "nowrap",
|
||||
paddingTop: "0px",
|
||||
@@ -150,12 +177,26 @@ const styles = (theme: Theme) => createStyles({
|
||||
|
||||
}),
|
||||
|
||||
noScrollGrid: css({
|
||||
position: "relative",
|
||||
left: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
paddingLeft: 30,
|
||||
paddingRight: 45,
|
||||
paddingTop: 0,
|
||||
|
||||
flex: "1 1 auto",
|
||||
}),
|
||||
|
||||
normalGrid: css({
|
||||
position: "relative",
|
||||
paddingLeft: 30,
|
||||
paddingRight: 30,
|
||||
paddingRight: 45,
|
||||
paddingTop: 8,
|
||||
|
||||
|
||||
flex: "1 1 auto",
|
||||
display: "flex", flexDirection: "row", flexWrap: "wrap",
|
||||
justifyContent: "flex-start", alignItems: "flex_start",
|
||||
@@ -171,7 +212,8 @@ const styles = (theme: Theme) => createStyles({
|
||||
// See the spacer div added after all controls in render() with provides the same effect.
|
||||
display: "flex", flexDirection: "row", flexWrap: "nowrap",
|
||||
justifyContent: "flex-start", alignItems: "flex-start",
|
||||
|
||||
position: "relative",
|
||||
|
||||
overflowX: "hidden",
|
||||
overflowY: "hidden",
|
||||
flex: "0 0 auto",
|
||||
@@ -218,7 +260,7 @@ const styles = (theme: Theme) => createStyles({
|
||||
elevation: 12,
|
||||
display: "flex",
|
||||
flexDirection: "row", flexWrap: "wrap",
|
||||
|
||||
|
||||
flex: "0 1 auto",
|
||||
}),
|
||||
portGroupLandscape: css({
|
||||
@@ -229,7 +271,7 @@ const styles = (theme: Theme) => createStyles({
|
||||
position: "relative",
|
||||
paddingLeft: 0,
|
||||
paddingRight: 0,
|
||||
|
||||
|
||||
paddingTop: 0,
|
||||
paddingBottom: 0,
|
||||
border: "2pt #AAA solid",
|
||||
@@ -238,7 +280,7 @@ const styles = (theme: Theme) => createStyles({
|
||||
display: "inline-flex",
|
||||
textOverflow: "ellipsis",
|
||||
flexDirection: "row", flexWrap: "nowrap",
|
||||
flex: "0 0 auto",
|
||||
flex: "0 0 auto",
|
||||
width: "fit-content",
|
||||
minWidth: "max-content"
|
||||
}),
|
||||
@@ -287,7 +329,8 @@ export type ControlNodes = (ReactNode | ControlGroup)[];
|
||||
|
||||
|
||||
export interface ControlViewCustomization {
|
||||
ModifyControls(controls: (React.ReactNode | ControlGroup)[]): (React.ReactNode | ControlGroup)[];
|
||||
modifyControls(controls: (React.ReactNode | ControlGroup)[]): (React.ReactNode | ControlGroup)[];
|
||||
fullScreen(): boolean;
|
||||
|
||||
}
|
||||
|
||||
@@ -407,11 +450,13 @@ const PluginControlView =
|
||||
/>
|
||||
));
|
||||
}
|
||||
private ixKey: number;
|
||||
|
||||
makeStandardControl(uiControl: UiControl, controlValues: ControlValue[]): ReactNode {
|
||||
let symbol = uiControl.symbol;
|
||||
if (!uiControl.is_input) {
|
||||
return (
|
||||
<PluginOutputControl instanceId={this.props.instanceId} uiControl={uiControl} />
|
||||
<PluginOutputControl key={uiControl.symbol } instanceId={this.props.instanceId} uiControl={uiControl} />
|
||||
|
||||
);
|
||||
}
|
||||
@@ -428,7 +473,7 @@ const PluginControlView =
|
||||
}
|
||||
return ((
|
||||
|
||||
<PluginControl instanceId={this.props.instanceId} uiControl={uiControl} value={controlValue.value}
|
||||
<PluginControl key={uiControl.symbol} instanceId={this.props.instanceId} uiControl={uiControl} value={controlValue.value}
|
||||
onChange={(value: number) => { this.onControlValueChanged(controlValue!.key, value) }}
|
||||
onPreviewChange={(value: number) => { this.onPreviewChange(controlValue!.key, value) }}
|
||||
requestIMEEdit={(uiControl: any, value: any) => this.requestImeEdit(uiControl, value)}
|
||||
@@ -438,27 +483,25 @@ const PluginControlView =
|
||||
|
||||
}
|
||||
|
||||
push_control(controls:ControlNodes, pluginControl: UiControl,controlValues: ControlValue[])
|
||||
{
|
||||
push_control(controls: ControlNodes, pluginControl: UiControl, controlValues: ControlValue[]) {
|
||||
// combine lamps with their previous control
|
||||
if ((pluginControl.isLamp() || pluginControl.isDbVu()) && controls.length !== 0)
|
||||
{
|
||||
if ((pluginControl.isLamp() || pluginControl.isDbVu()) && controls.length !== 0) {
|
||||
const classes = withStyles.getClasses(this.props);
|
||||
let newControl = this.makeStandardControl(pluginControl,controlValues);
|
||||
let previousControl = controls[controls.length-1];
|
||||
let newControl = this.makeStandardControl(pluginControl, controlValues);
|
||||
let previousControl = controls[controls.length - 1];
|
||||
if (!(previousControl instanceof ControlGroup)) {
|
||||
let pair = (
|
||||
<div className={classes.controlPair}>
|
||||
<div key={"k"+this.ixKey++} className={classes.controlPair}>
|
||||
{previousControl as ReactNode}
|
||||
{newControl}
|
||||
</div>
|
||||
);
|
||||
controls[controls.length-1] = pair;
|
||||
// push a spacer control in order to make placing of extended controls predictable.
|
||||
controls[controls.length - 1] = pair;
|
||||
// push a spacer control in order to make placing of extended controls predictable.
|
||||
// (e.g.. inserting at position 4 still places the extended control after four previous controls
|
||||
controls.push((
|
||||
|
||||
<div className={classes.controlSpacer} />
|
||||
<div key={"k"+this.ixKey++} className={classes.controlSpacer} />
|
||||
));
|
||||
} else {
|
||||
controls.push(newControl);
|
||||
@@ -492,7 +535,7 @@ const PluginControlView =
|
||||
pluginControl = plugin.controls[i];
|
||||
|
||||
if (!pluginControl.isHidden()) {
|
||||
this.push_control(groupControls,pluginControl,controlValues);
|
||||
this.push_control(groupControls, pluginControl, controlValues);
|
||||
indexes.push(pluginControl.index);
|
||||
}
|
||||
}
|
||||
@@ -502,7 +545,7 @@ const PluginControlView =
|
||||
)
|
||||
portGroupMap[pluginControl.port_group] = controlGroup;
|
||||
} else {
|
||||
this.push_control(result,pluginControl,controlValues);
|
||||
this.push_control(result, pluginControl, controlValues);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -647,8 +690,8 @@ const PluginControlView =
|
||||
result.push((
|
||||
<div key={"ctl" + (this.controlKeyIndex++)} className={!isLandscapeGrid ? classes.portGroup : classes.portGroupLandscape}>
|
||||
<div className={classes.portGroupTitle}>
|
||||
<Tooltip title={controlGroup.name}
|
||||
placement="top-start" arrow enterDelay={1500} enterNextDelay={1500}
|
||||
<Tooltip title={controlGroup.name}
|
||||
placement="top-start" arrow enterDelay={1500} enterNextDelay={1500}
|
||||
>
|
||||
<Typography noWrap variant="caption" >{controlGroup.name}</Typography>
|
||||
</Tooltip>
|
||||
@@ -663,11 +706,21 @@ const PluginControlView =
|
||||
));
|
||||
|
||||
} else {
|
||||
result.push((
|
||||
<div key={"ctl" + (this.controlKeyIndex++)} className={hasGroups ? classes.portgroupControlPadding : classes.controlPadding} >
|
||||
{node as ReactNode}
|
||||
</div>
|
||||
));
|
||||
if (this.fullScreen())
|
||||
{
|
||||
result.push(
|
||||
<div style={{position: "relative", width: "100%", height:"100%"}}
|
||||
>
|
||||
{node as ReactNode}
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
result.push((
|
||||
<div key={"ctl" + (this.controlKeyIndex++)} className={hasGroups ? classes.portgroupControlPadding : classes.controlPadding} >
|
||||
{node as ReactNode}
|
||||
</div>
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -686,7 +739,7 @@ const PluginControlView =
|
||||
}
|
||||
return (
|
||||
<MidiChannelBindingControl key="channelBindingCtl" midiChannelBinding={pedalboardItem.midiChannelBinding}
|
||||
onChange={(result)=> {
|
||||
onChange={(result) => {
|
||||
|
||||
}}
|
||||
/>
|
||||
@@ -694,6 +747,12 @@ const PluginControlView =
|
||||
}
|
||||
|
||||
|
||||
fullScreen() {
|
||||
if (this.props.customization) {
|
||||
return this.props.customization.fullScreen();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
render(): ReactNode {
|
||||
this.controlKeyIndex = 0;
|
||||
@@ -734,20 +793,26 @@ const PluginControlView =
|
||||
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;
|
||||
}
|
||||
|
||||
controlNodes = this.getStandardControlNodes(plugin, controlValues);
|
||||
|
||||
if (this.props.customization) {
|
||||
// allow wrapper class to insert/remove/rebuild controls.
|
||||
controlNodes = this.props.customization.ModifyControls(controlNodes);
|
||||
controlNodes = this.props.customization.modifyControls(controlNodes);
|
||||
}
|
||||
|
||||
let nodes = this.controlNodesToNodes(controlNodes);
|
||||
|
||||
if (plugin.has_midi_input && !pedalboardItem.midiChannelBinding)
|
||||
{
|
||||
if (plugin.has_midi_input && !pedalboardItem.midiChannelBinding) {
|
||||
pedalboardItem.midiChannelBinding = MidiChannelBinding.CreateMissingValue();
|
||||
|
||||
|
||||
}
|
||||
if (pedalboardItem.midiChannelBinding) {
|
||||
nodes.push(this.midiBindingControl(pedalboardItem));
|
||||
@@ -755,7 +820,7 @@ const PluginControlView =
|
||||
|
||||
|
||||
return (
|
||||
<div className={classes.frame}>
|
||||
<div className={frameClass}>
|
||||
<div className={classes.vuMeterL}>
|
||||
<VuMeter displayText={true} display="input" instanceId={pedalboardItem.instanceId} />
|
||||
</div>
|
||||
@@ -768,9 +833,11 @@ const PluginControlView =
|
||||
nodes
|
||||
}
|
||||
{/* Extra space to allow scrolling right to the end in lascape especially */}
|
||||
<div style={{ flex: "0 0 40px", width: 40, height: 40 }} />
|
||||
{!this.fullScreen() && (
|
||||
<div style={{ flex: "0 0 40px", width: 40, height: 40 }} />
|
||||
)}
|
||||
{
|
||||
(!this.state.landscapeGrid) && (
|
||||
(!this.state.landscapeGrid) && (!this.fullScreen()) && (
|
||||
<div style={{ flex: "0 1 100%", width: "0px", height: 40 }} />
|
||||
)
|
||||
}
|
||||
@@ -785,7 +852,7 @@ const PluginControlView =
|
||||
onCancel={() => {
|
||||
this.setState({ showFileDialog: false });
|
||||
}}
|
||||
onApply={(fileProperty,selectedFile) => {
|
||||
onApply={(fileProperty, selectedFile) => {
|
||||
this.model.setPatchProperty(
|
||||
this.props.instanceId,
|
||||
fileProperty.patchProperty,
|
||||
|
||||
@@ -60,8 +60,11 @@ const ToobCabSimView =
|
||||
this.state = {
|
||||
}
|
||||
}
|
||||
fullScreen() {
|
||||
return false;
|
||||
}
|
||||
|
||||
ModifyControls(controls: (React.ReactNode| ControlGroup)[]): (React.ReactNode| ControlGroup)[]
|
||||
modifyControls(controls: (React.ReactNode| ControlGroup)[]): (React.ReactNode| ControlGroup)[]
|
||||
{
|
||||
controls.splice(0,0,
|
||||
( <ToobFrequencyResponseView instanceId={this.props.instanceId} />)
|
||||
|
||||
@@ -60,8 +60,11 @@ const ToobInputStageView =
|
||||
this.state = {
|
||||
}
|
||||
}
|
||||
fullScreen() {
|
||||
return false;
|
||||
}
|
||||
|
||||
ModifyControls(controls: (React.ReactNode| ControlGroup)[]): (React.ReactNode| ControlGroup)[]
|
||||
modifyControls(controls: (React.ReactNode| ControlGroup)[]): (React.ReactNode| ControlGroup)[]
|
||||
{
|
||||
return controls;
|
||||
// let group = controls[1] as ControlGroup;
|
||||
|
||||
@@ -103,8 +103,11 @@ const ToobMLView =
|
||||
componentWillUnmount() {
|
||||
this.removeGainEnabledSubscription();
|
||||
}
|
||||
fullScreen() {
|
||||
return false;
|
||||
}
|
||||
|
||||
ModifyControls(controls: (React.ReactNode | ControlGroup)[]): (React.ReactNode | ControlGroup)[] {
|
||||
modifyControls(controls: (React.ReactNode | ControlGroup)[]): (React.ReactNode | ControlGroup)[] {
|
||||
// Find EQ group
|
||||
// let group = controls.find((control) => typeof control !== 'string' && (control as ControlGroup).name === "EQ") as ControlGroup;
|
||||
// if (group) {
|
||||
|
||||
@@ -0,0 +1,733 @@
|
||||
/*
|
||||
* 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, { useEffect } from 'react';
|
||||
import { styled } from '@mui/material/styles';
|
||||
import Box from '@mui/material/Box';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import Slider from '@mui/material/Slider';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import Pause from '@mui/icons-material/Pause';
|
||||
import PlayArrow from '@mui/icons-material/PlayArrow';
|
||||
import FastForward from '@mui/icons-material/FastForward';
|
||||
import FastRewind from '@mui/icons-material/FastRewind';
|
||||
import { PiPedalModelFactory } from './PiPedalModel';
|
||||
import ButtonTooltip from './ButtonTooltip';
|
||||
import { pathFileNameOnly } from './FilePropertyDialog'
|
||||
import ButtonBase from '@mui/material/ButtonBase';
|
||||
import FilePropertyDialog from './FilePropertyDialog';
|
||||
import JsonAtom from './JsonAtom';
|
||||
import { UiFileProperty } from './Lv2Plugin';
|
||||
import { Divider } from '@mui/material';
|
||||
import useWindowSize from './UseWindowSize';
|
||||
|
||||
let Player__seek = "http://two-play.com/plugins/toob-player#seek"
|
||||
const AUDIO_FILE_PROPERTY_URI = "http://two-play.com/plugins/toob-player#audioFile";
|
||||
class PluginState {
|
||||
// Must match PluginState values in ToobPlayer.hpp
|
||||
static Idle = 0.0;
|
||||
static CuePlaying = 1;
|
||||
static CuePlayPaused = 2.0;
|
||||
static Pausing = 3;
|
||||
static Paused = 4;
|
||||
static Playing = 5;
|
||||
static Error = 6;
|
||||
};
|
||||
|
||||
const WallPaper = styled('div')({
|
||||
position: 'absolute',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
top: 0,
|
||||
left: 0,
|
||||
overflow: 'hidden',
|
||||
background: 'linear-gradient(rgb(255, 38, 142) 0%, rgb(255, 105, 79) 100%)',
|
||||
transition: 'all 500ms cubic-bezier(0.175, 0.885, 0.32, 1.275) 0s',
|
||||
'&::before': {
|
||||
content: '""',
|
||||
width: '140%',
|
||||
height: '140%',
|
||||
position: 'absolute',
|
||||
top: '-40%',
|
||||
right: '-50%',
|
||||
background:
|
||||
'radial-gradient(at center center, rgb(62, 79, 249) 0%, rgba(62, 79, 249, 0) 64%)',
|
||||
},
|
||||
'&::after': {
|
||||
content: '""',
|
||||
width: '140%',
|
||||
height: '140%',
|
||||
position: 'absolute',
|
||||
bottom: '-50%',
|
||||
left: '-30%',
|
||||
background:
|
||||
'radial-gradient(at center center, rgb(247, 237, 225) 0%, rgba(247, 237, 225, 0) 70%)',
|
||||
transform: 'rotate(30deg)',
|
||||
},
|
||||
});
|
||||
|
||||
function jsonToPath(json: string) {
|
||||
try {
|
||||
let o = JSON.parse(json);
|
||||
return o.value;
|
||||
}
|
||||
catch (_) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
interface WidgetProps {
|
||||
noBorders: boolean;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const WidgetBorders = styled('div')(({ theme }) => ({
|
||||
|
||||
padding: 16,
|
||||
borderRadius: 16,
|
||||
minWidth: 300,
|
||||
maxWidth: 800,
|
||||
width: "70%",
|
||||
margin: 'auto',
|
||||
position: 'relative',
|
||||
zIndex: 1,
|
||||
backgroundColor: 'rgba(255,255,255,0.4)',
|
||||
boxShadow: "1px 3px 20px #0008",
|
||||
...theme.applyStyles('dark', {
|
||||
backgroundColor: 'rgba(0,0,0,0.6)',
|
||||
}),
|
||||
}));
|
||||
|
||||
const WidgetNoBorders = styled('div')(({ theme }) => ({
|
||||
|
||||
padding: 16,
|
||||
borderRadius: 0,
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
margin: 0,
|
||||
position: 'relative',
|
||||
zIndex: 1,
|
||||
backgroundColor: 'rgba(255,255,255,0.4)',
|
||||
...theme.applyStyles('dark', {
|
||||
backgroundColor: 'rgba(0,0,0,0.6)',
|
||||
}),
|
||||
}));
|
||||
|
||||
|
||||
function Widget(props: WidgetProps) {
|
||||
if (props.noBorders) {
|
||||
return (<WidgetNoBorders>{props.children} </WidgetNoBorders>);
|
||||
} else {
|
||||
return (<WidgetBorders>{props.children} </WidgetBorders>);
|
||||
}
|
||||
}
|
||||
|
||||
const CoverImage = styled('div')({
|
||||
width: 60,
|
||||
height: 60,
|
||||
objectFit: 'cover',
|
||||
overflow: 'hidden',
|
||||
flexShrink: 0,
|
||||
borderRadius: 8,
|
||||
backgroundColor: 'rgba(0,0,0,0.08)',
|
||||
'& > img': {
|
||||
width: '100%',
|
||||
},
|
||||
});
|
||||
|
||||
const TinyText = styled(Typography)({
|
||||
fontSize: '0.85rem',
|
||||
opacity: 0.9,
|
||||
fontWeight: 400,
|
||||
letterSpacing: 0.2,
|
||||
});
|
||||
|
||||
|
||||
export interface ToobPlayerControlProps {
|
||||
instanceId: number;
|
||||
extraControls: React.ReactElement[];
|
||||
}
|
||||
|
||||
export default function ToobPlayerControl(
|
||||
props: ToobPlayerControlProps
|
||||
) {
|
||||
const defaultCoverArt = "/img/default_album.jpg";
|
||||
const [duration, setDuration] = React.useState(0.0);
|
||||
const [position, setPosition] = React.useState(0.0);
|
||||
const [dragging, setDragging] = React.useState(false);
|
||||
const [pluginState, setPluginState] = React.useState(0.0);
|
||||
const [sliderValue, setSliderValue] = React.useState(0.0);
|
||||
const [coverArt, setCoverArt] = React.useState(defaultCoverArt);
|
||||
const [audioFile, setAudioFile] = React.useState("");
|
||||
const [title, setTitle] = React.useState("");
|
||||
const [album, setAlbum] = React.useState("");
|
||||
const [showFileDialog, setShowFileDialog] = React.useState(false);
|
||||
|
||||
const model = PiPedalModelFactory.getInstance();
|
||||
|
||||
const [size] = useWindowSize();
|
||||
const width = size.width;
|
||||
const height = size.height;
|
||||
|
||||
const HORIZONTAL_CONTROL_SCROLL_HEIGHT_BREAK = 500;
|
||||
|
||||
const useHorizontalLayout = height < HORIZONTAL_CONTROL_SCROLL_HEIGHT_BREAK && width > 573 && width > height;
|
||||
const useVerticalScroll = width < 573;
|
||||
const useQuadMixPanel = width < 720;
|
||||
const noBorders = width < 420 || height < 720;
|
||||
|
||||
function SelectFile() {
|
||||
setShowFileDialog(true);
|
||||
}
|
||||
function formatDuration(value_: number) {
|
||||
let value = Math.ceil(value_);
|
||||
const minute = Math.floor(value / 60);
|
||||
const secondLeft = value - minute * 60;
|
||||
return `${minute}:${secondLeft < 10 ? `0${secondLeft}` : secondLeft}`;
|
||||
}
|
||||
function onAudioFileChanged(path: string) {
|
||||
setAudioFile(path);
|
||||
model.getAudioFileMetadata(path)
|
||||
.then((metadata) => {
|
||||
if (metadata.track !== "") {
|
||||
let track = metadata.track;
|
||||
if (!track.endsWith('.')) {
|
||||
track += '.';
|
||||
}
|
||||
setTitle(track + " " + metadata.title);
|
||||
}
|
||||
else {
|
||||
setTitle(metadata.title);
|
||||
}
|
||||
setAlbum(metadata.album);
|
||||
})
|
||||
.catch(()=>{
|
||||
setTitle("#error");
|
||||
setAlbum("");
|
||||
});
|
||||
|
||||
}
|
||||
function ControlCluster() {
|
||||
return (
|
||||
<Box
|
||||
sx={(theme) => ({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
mt: -1,
|
||||
'& svg': {
|
||||
color: '#000',
|
||||
...theme.applyStyles('dark', {
|
||||
color: '#fff',
|
||||
}),
|
||||
},
|
||||
})}
|
||||
>
|
||||
<ButtonTooltip title="Previous track">
|
||||
<IconButton
|
||||
style={{ opacity: (pluginState === PluginState.Idle ? 0.2 : 0.7) }}
|
||||
onClick={() => {
|
||||
if (position > 3) {
|
||||
model.sendPedalboardControlTrigger(
|
||||
props.instanceId,
|
||||
"stop",
|
||||
1
|
||||
);
|
||||
} else {
|
||||
onPreviousTrack();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<FastRewind fontSize="large" />
|
||||
</IconButton>
|
||||
</ButtonTooltip>
|
||||
<ButtonTooltip title="Play/Pause">
|
||||
<IconButton
|
||||
|
||||
style={{ opacity: (pluginState === PluginState.Idle ? 0.2 : 0.7) }}
|
||||
onClick={() => {
|
||||
if (pluginState != PluginState.Idle) {
|
||||
if (paused) {
|
||||
model.sendPedalboardControlTrigger(
|
||||
props.instanceId,
|
||||
"play",
|
||||
1
|
||||
);
|
||||
} else {
|
||||
model.sendPedalboardControlTrigger(
|
||||
props.instanceId,
|
||||
"pause",
|
||||
1
|
||||
);
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
{paused ? (
|
||||
<PlayArrow sx={{ fontSize: '3rem' }} />
|
||||
) : (
|
||||
<Pause sx={{ fontSize: '3rem' }} />
|
||||
)}
|
||||
</IconButton>
|
||||
</ButtonTooltip>
|
||||
<ButtonTooltip title="Next track">
|
||||
<IconButton
|
||||
style={{ opacity: (pluginState == PluginState.Idle ? 0.2 : 0.7) }}
|
||||
onClick={() => { onNextTrack(); }}
|
||||
>
|
||||
<FastForward fontSize="large" />
|
||||
</IconButton>
|
||||
</ButtonTooltip>
|
||||
</Box>
|
||||
|
||||
);
|
||||
}
|
||||
function FilePanel() {
|
||||
return (
|
||||
<ButtonBase style={{ display: "block", width: "100%", borderRadius: 10, textAlign: "left" }}
|
||||
onClick={() => { SelectFile() }}
|
||||
>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', padding: "2px" }}>
|
||||
<CoverImage>
|
||||
<img style={{ opacity: pluginState === PluginState.Idle ? 0.3 : 1.0 }}
|
||||
src={
|
||||
coverArt
|
||||
}
|
||||
/>
|
||||
</CoverImage>
|
||||
<Box sx={{ ml: 1.5, minWidth: 0 }}>
|
||||
{pluginState === PluginState.Idle ? (
|
||||
<Typography variant="caption" noWrap>
|
||||
Tap to select
|
||||
</Typography>
|
||||
) : (
|
||||
<div>
|
||||
<Typography variant="body1" noWrap>
|
||||
{effectiveTitle}
|
||||
</Typography>
|
||||
<Typography variant="body2" noWrap sx={{}}>
|
||||
{album}
|
||||
</Typography>
|
||||
</div>
|
||||
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</ButtonBase>
|
||||
);
|
||||
|
||||
}
|
||||
function getUiFileProperty(uri: string): UiFileProperty {
|
||||
let pedalboardItem = model.pedalboard.get().getItem(props.instanceId);
|
||||
let uiPlugin = model.getUiPlugin(pedalboardItem.uri);
|
||||
if (!uiPlugin) {
|
||||
throw "uiPlugin not found.";
|
||||
}
|
||||
for (let property of uiPlugin.fileProperties) {
|
||||
if (property.patchProperty === uri) {
|
||||
return property;
|
||||
}
|
||||
}
|
||||
throw "FileProperty not found.";
|
||||
}
|
||||
function onNextTrack() {
|
||||
model.getNextAudioFile(audioFile)
|
||||
.then((file) => {
|
||||
let json = JsonAtom.Path(file);
|
||||
model.setPatchProperty(
|
||||
props.instanceId,
|
||||
AUDIO_FILE_PROPERTY_URI,
|
||||
json);
|
||||
})
|
||||
.catch(() => {
|
||||
});
|
||||
|
||||
}
|
||||
function onPreviousTrack() {
|
||||
model.getPreviousAudioFile(audioFile)
|
||||
.then((file) => {
|
||||
let json = JsonAtom.Path(file);
|
||||
model.setPatchProperty(
|
||||
props.instanceId,
|
||||
AUDIO_FILE_PROPERTY_URI,
|
||||
json);
|
||||
})
|
||||
.catch(() => {
|
||||
});
|
||||
}
|
||||
function OnSeek(value: number) {
|
||||
setDragging(true);
|
||||
model.setPatchProperty(props.instanceId, Player__seek, value)
|
||||
.then(() => {
|
||||
setPosition(value);
|
||||
setDragging(false);
|
||||
}).catch((e) => {
|
||||
console.warn("Seek error. " + e.toString());
|
||||
setPosition(value);
|
||||
setDragging(false);
|
||||
});
|
||||
setSliderValue(value);
|
||||
}
|
||||
useEffect(() => {
|
||||
let durationHandle = model.monitorPort(props.instanceId, "duration", 1.0 / 15,
|
||||
(value) => {
|
||||
setDuration(value);
|
||||
}
|
||||
);
|
||||
let positionHandle = model.monitorPort(props.instanceId, "position", 1.0,
|
||||
(value) => {
|
||||
setPosition(value);
|
||||
}
|
||||
);
|
||||
let pluginStateHandle = model.monitorPort(props.instanceId, "state", 1.0 / 1000.0,
|
||||
(value) => {
|
||||
setPluginState(value);
|
||||
}
|
||||
);
|
||||
let filePropertyHandle = model.monitorPatchProperty(
|
||||
props.instanceId,
|
||||
AUDIO_FILE_PROPERTY_URI,
|
||||
(instanceId: number, propertyUri: string, atomObject: any) => {
|
||||
if (typeof (atomObject) === "object") {
|
||||
let path = atomObject.value;
|
||||
onAudioFileChanged(path);
|
||||
} else if (typeof(atomObject) === "string")
|
||||
{
|
||||
onAudioFileChanged(atomObject as string);
|
||||
}
|
||||
}
|
||||
|
||||
);
|
||||
|
||||
model.getPatchProperty(props.instanceId, AUDIO_FILE_PROPERTY_URI)
|
||||
.then((o) => {
|
||||
let path = o.value;
|
||||
onAudioFileChanged(path);
|
||||
})
|
||||
|
||||
|
||||
return () => {
|
||||
model.unmonitorPort(durationHandle);
|
||||
model.unmonitorPort(positionHandle);
|
||||
model.unmonitorPort(pluginStateHandle);
|
||||
model.cancelMonitorPatchProperty(filePropertyHandle);
|
||||
};
|
||||
},
|
||||
[]
|
||||
);
|
||||
const effectivePosition =
|
||||
(pluginState !== PluginState.Idle) ? Math.min(position, duration) : 0.0;
|
||||
const effectiveTitle = title !== "" ? title : pathFileNameOnly(audioFile);
|
||||
|
||||
const paused = (pluginState === PluginState.Idle
|
||||
|| pluginState === PluginState.CuePlayPaused
|
||||
|| pluginState === PluginState.Paused);
|
||||
|
||||
function LinearMixPanel() {
|
||||
return (
|
||||
<Box style={{ width: "100%" }} >
|
||||
<div style={{ display: "flex", flex: "0 0 auto", justifyContent: "center", alignItems: 'center', flexFlow: "row nowrap" }}>
|
||||
{props.extraControls}
|
||||
</div>
|
||||
</Box>
|
||||
);
|
||||
|
||||
}
|
||||
function QuadMixPanel() {
|
||||
return (
|
||||
<div style={{ display: "flex", width: "100%", flex: "0 0 auto", justifyContent: "center", alignItems: "center", flexFlow: "row nowrap" }}>
|
||||
<div style={{ display: "flex", flex: "0 0 auto", gap: 8, alignItems: "center", flexFlow: "column nowrap" }}>
|
||||
{props.extraControls[0]}
|
||||
{props.extraControls[1]}
|
||||
</div>
|
||||
<div style={{ display: "flex", flex: "0 0 auto", gap: 8, alignItems: "center", flexFlow: "column nowrap" }}>
|
||||
{props.extraControls[2]}
|
||||
{props.extraControls[3]}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
function SliderCluster() {
|
||||
return (
|
||||
|
||||
<div style={{ display: "flex", flexFlow: "column nowrap" }}>
|
||||
<Slider
|
||||
value={dragging ? sliderValue : effectivePosition}
|
||||
min={0}
|
||||
step={1}
|
||||
max={duration}
|
||||
onChange={(_, val) => {
|
||||
let v = val as number;
|
||||
setPosition(v);
|
||||
setSliderValue(v);
|
||||
}}
|
||||
onChangeCommitted={(e, value) => {
|
||||
let v = value as number;
|
||||
OnSeek(v);
|
||||
}}
|
||||
onPointerDown={(e) => {
|
||||
setDragging(true);
|
||||
}}
|
||||
onPointerUp={(e) => {
|
||||
// setDragging(false);
|
||||
}}
|
||||
disabled={duration == 0}
|
||||
sx={(t) => ({
|
||||
width: "100%",
|
||||
color: 'rgba(0,0,0,0.87)',
|
||||
height: 4,
|
||||
'& .MuiSlider-thumb': {
|
||||
width: 8,
|
||||
height: 8,
|
||||
transition: '0.3s cubic-bezier(.47,1.64,.41,.8)',
|
||||
'&::before': {
|
||||
boxShadow: '0 2px 12px 0 rgba(0,0,0,0.4)',
|
||||
},
|
||||
'&:hover, &.Mui-focusVisible': {
|
||||
boxShadow: `0px 0px 0px 8px ${'rgb(0 0 0 / 16%)'}`,
|
||||
...t.applyStyles('dark', {
|
||||
boxShadow: `0px 0px 0px 8px ${'rgb(255 255 255 / 16%)'}`,
|
||||
}),
|
||||
},
|
||||
'&.Mui-active': {
|
||||
width: 20,
|
||||
height: 20,
|
||||
},
|
||||
},
|
||||
'& .MuiSlider-rail': {
|
||||
opacity: 0.28,
|
||||
},
|
||||
...t.applyStyles('dark', {
|
||||
color: '#fff',
|
||||
}),
|
||||
})}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
flex: "0 0 auto",
|
||||
width: "100%",
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
marginTop: -4,
|
||||
}}
|
||||
>
|
||||
<TinyText>{formatDuration(dragging ? sliderValue : effectivePosition)}</TinyText>
|
||||
<TinyText>{formatDuration(duration)}</TinyText>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
function VerticalWidget() {
|
||||
return (
|
||||
<div style={{
|
||||
position: "relative", width: "100%", height: "100%",
|
||||
display: "flex", flexFlow: "column nowrap"
|
||||
}} >
|
||||
<div style={{ flex: "1 1 1px" }} />
|
||||
|
||||
<Widget noBorders={noBorders}>
|
||||
{
|
||||
FilePanel()
|
||||
}
|
||||
<Slider
|
||||
value={dragging ? sliderValue : effectivePosition}
|
||||
min={0}
|
||||
step={1}
|
||||
max={duration}
|
||||
onChange={(_, val) => {
|
||||
let v = val as number;
|
||||
setPosition(v);
|
||||
setSliderValue(v);
|
||||
}}
|
||||
onChangeCommitted={(e, value) => {
|
||||
let v = value as number;
|
||||
OnSeek(v);
|
||||
}}
|
||||
onPointerDown={(e) => {
|
||||
setDragging(true);
|
||||
}}
|
||||
onPointerUp={(e) => {
|
||||
// setDragging(false);
|
||||
}}
|
||||
disabled={duration == 0}
|
||||
sx={(t) => ({
|
||||
width: "100%",
|
||||
color: 'rgba(0,0,0,0.87)',
|
||||
height: 4,
|
||||
'& .MuiSlider-thumb': {
|
||||
width: 8,
|
||||
height: 8,
|
||||
transition: '0.3s cubic-bezier(.47,1.64,.41,.8)',
|
||||
'&::before': {
|
||||
boxShadow: '0 2px 12px 0 rgba(0,0,0,0.4)',
|
||||
},
|
||||
'&:hover, &.Mui-focusVisible': {
|
||||
boxShadow: `0px 0px 0px 8px ${'rgb(0 0 0 / 16%)'}`,
|
||||
...t.applyStyles('dark', {
|
||||
boxShadow: `0px 0px 0px 8px ${'rgb(255 255 255 / 16%)'}`,
|
||||
}),
|
||||
},
|
||||
'&.Mui-active': {
|
||||
width: 20,
|
||||
height: 20,
|
||||
},
|
||||
},
|
||||
'& .MuiSlider-rail': {
|
||||
opacity: 0.28,
|
||||
},
|
||||
...t.applyStyles('dark', {
|
||||
color: '#fff',
|
||||
}),
|
||||
})}
|
||||
/>
|
||||
<Box
|
||||
sx={{
|
||||
width: "100%",
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
mt: -2,
|
||||
}}
|
||||
>
|
||||
<TinyText>{formatDuration(dragging ? sliderValue : effectivePosition)}</TinyText>
|
||||
<TinyText>{formatDuration(duration)}</TinyText>
|
||||
</Box>
|
||||
{
|
||||
ControlCluster()
|
||||
}
|
||||
<Divider style={{ marginTop: 8, marginBottom: 8 }} />
|
||||
{useQuadMixPanel ? QuadMixPanel()
|
||||
:
|
||||
LinearMixPanel()
|
||||
}
|
||||
</Widget>
|
||||
<div style={{ flex: "2 2 1px" }} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function HorizontalWidget() {
|
||||
return (
|
||||
<div style={{
|
||||
position: "relative", width: "100%", height: "100%",
|
||||
display: "flex", flexFlow: "column nowrap"
|
||||
}} >
|
||||
|
||||
<Widget noBorders={true}>
|
||||
<div style={{
|
||||
display: "flex", flexFlow: "row nowrap", alignItems: "center", justifyContent: "stretch",
|
||||
width: "100%"
|
||||
}}>
|
||||
<div style={{
|
||||
display: "flex", alignItems: "stretch", flexFlow: "column nowrap", flex: "1 1 100%"
|
||||
|
||||
}}>
|
||||
<div style={{ display: "flex", flexFlow: "row nowrap", flex: "1 1 auto" }}>
|
||||
<div style={{ position: "relative", display: "flex", flexFlow: "row nowrap", flex: "1 1 auto" }}>
|
||||
{
|
||||
FilePanel()
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ flex: "0 0 auto" }}>
|
||||
{
|
||||
SliderCluster()
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div style={{
|
||||
position: "relative", display: "flex", flexFlow: "row nowrap", flex: "0 0 auto"
|
||||
|
||||
}}>
|
||||
{
|
||||
ControlCluster()
|
||||
}
|
||||
</div>
|
||||
<div style={{ flex: "0 0 auto" }}>
|
||||
{LinearMixPanel()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</Widget>
|
||||
<div style={{ flex: "2 2 1px" }} />
|
||||
</div >
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div id="toobPlayerViewFrame" style={{
|
||||
width: '100%', height: "100%", overflow: 'hidden', position: 'relative',
|
||||
}}>
|
||||
<WallPaper />
|
||||
{
|
||||
useHorizontalLayout ?
|
||||
HorizontalWidget() : VerticalWidget()
|
||||
}
|
||||
{showFileDialog && (
|
||||
<FilePropertyDialog open={showFileDialog}
|
||||
fileProperty={getUiFileProperty(AUDIO_FILE_PROPERTY_URI)}
|
||||
instanceId={props.instanceId}
|
||||
selectedFile={audioFile}
|
||||
onCancel={() => {
|
||||
setShowFileDialog(false);
|
||||
}}
|
||||
onApply={(fileProperty, selectedFile) => {
|
||||
model.setPatchProperty(
|
||||
props.instanceId,
|
||||
AUDIO_FILE_PROPERTY_URI,
|
||||
JsonAtom.Path(selectedFile)
|
||||
)
|
||||
.then(() => {
|
||||
|
||||
})
|
||||
.catch((error) => {
|
||||
model.showAlert("Unable to complete the operation. " + error);
|
||||
});
|
||||
|
||||
}}
|
||||
onOk={(fileProperty, selectedFile) => {
|
||||
|
||||
model.setPatchProperty(
|
||||
props.instanceId,
|
||||
fileProperty.patchProperty,
|
||||
JsonAtom.Path(selectedFile)
|
||||
)
|
||||
.then(() => {
|
||||
|
||||
})
|
||||
.catch((error) => {
|
||||
model.showAlert("Unable to complete the operation. " + error);
|
||||
});
|
||||
setShowFileDialog(false);
|
||||
}
|
||||
}
|
||||
/>
|
||||
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
// Copyright (c) 2022 Robin Davies
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
// this software and associated documentation files (the "Software"), to deal in
|
||||
// the Software without restriction, including without limitation the rights to
|
||||
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
// the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
// subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
import React from 'react';
|
||||
import { Theme } from '@mui/material/styles';
|
||||
|
||||
import WithStyles from './WithStyles';
|
||||
import { createStyles } from './WithStyles';
|
||||
|
||||
import { withStyles } from "tss-react/mui";
|
||||
|
||||
import IControlViewFactory from './IControlViewFactory';
|
||||
import { PiPedalModelFactory, PiPedalModel, MonitorPortHandle } from "./PiPedalModel";
|
||||
import { PedalboardItem } from './Pedalboard';
|
||||
import PluginControlView, { ControlGroup, ControlViewCustomization } from './PluginControlView';
|
||||
// import ToobFrequencyResponseView from './ToobFrequencyResponseView';
|
||||
import ToobPlayerControl from './ToobPlayerControl';
|
||||
|
||||
const styles = (theme: Theme) => createStyles({
|
||||
});
|
||||
|
||||
interface ToobPlayerProps extends WithStyles<typeof styles> {
|
||||
instanceId: number;
|
||||
item: PedalboardItem;
|
||||
|
||||
}
|
||||
interface ToobPlayerState {
|
||||
playPosition: number;
|
||||
duration: number;
|
||||
}
|
||||
|
||||
const ToobPlayerView =
|
||||
withStyles(
|
||||
class extends React.Component<ToobPlayerProps, ToobPlayerState>
|
||||
implements ControlViewCustomization {
|
||||
model: PiPedalModel;
|
||||
gainRef: React.RefObject<HTMLDivElement | null>;
|
||||
|
||||
customizationId: number = 1;
|
||||
|
||||
constructor(props: ToobPlayerProps) {
|
||||
super(props);
|
||||
this.model = PiPedalModelFactory.getInstance();
|
||||
this.gainRef = React.createRef();
|
||||
this.state = {
|
||||
playPosition: 0.0,
|
||||
duration: 0.0
|
||||
}
|
||||
}
|
||||
|
||||
subscribedId?: number = undefined;
|
||||
monitorPlayPositionHandle?: MonitorPortHandle = undefined;
|
||||
monitorDurationHandle?: MonitorPortHandle = undefined;
|
||||
removePortSubscriptions() {
|
||||
|
||||
if (this.monitorPlayPositionHandle) {
|
||||
this.model.unmonitorPort(this.monitorPlayPositionHandle);
|
||||
this.monitorPlayPositionHandle = undefined;
|
||||
}
|
||||
}
|
||||
addPortSubscriptions(instanceId: number) {
|
||||
this.removePortSubscriptions();
|
||||
this.subscribedId = instanceId;
|
||||
this.monitorPlayPositionHandle = this.model.monitorPort(instanceId, "position", 1 / 15.0,
|
||||
(value: number) => {
|
||||
this.setState({ playPosition: value });
|
||||
});
|
||||
this.monitorDurationHandle = this.model.monitorPort(instanceId, "duration", 1 / 15.0,
|
||||
(value: number) => {
|
||||
this.setState({ duration: value });
|
||||
});
|
||||
}
|
||||
|
||||
// componentDidUpdate() {
|
||||
// if (this.props.instanceId !== this.subscribedId) {
|
||||
// this.removeGainEnabledSubscription();
|
||||
// this.addGainEnabledSubscription(this.props.instanceId);
|
||||
|
||||
// }
|
||||
// }
|
||||
componentDidMount() {
|
||||
this.addPortSubscriptions(this.props.instanceId);
|
||||
}
|
||||
componentWillUnmount() {
|
||||
this.removePortSubscriptions();
|
||||
}
|
||||
fullScreen() {
|
||||
return true;
|
||||
}
|
||||
modifyControls(controls: (React.ReactNode | ControlGroup)[]): (React.ReactNode | ControlGroup)[] {
|
||||
let extraControls: React.ReactElement[] = [];
|
||||
let mixPanel = controls[3] as ControlGroup;
|
||||
let iKey = 0;
|
||||
for (let mixControl of mixPanel.controls) {
|
||||
extraControls.push(
|
||||
(
|
||||
<div key={"k"+ iKey++} style={{flex: "0 0 auto", position: "relative",height: "100%" }}>
|
||||
{mixControl}
|
||||
</div>
|
||||
));
|
||||
}
|
||||
|
||||
let panel = (
|
||||
<ToobPlayerControl key="MusicPlayer" instanceId={this.props.instanceId} extraControls={extraControls} />
|
||||
);
|
||||
|
||||
let result: (React.ReactNode | ControlGroup)[] = [];
|
||||
result.push(panel);
|
||||
return result;
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<PluginControlView
|
||||
instanceId={this.props.instanceId}
|
||||
item={this.props.item}
|
||||
customization={this}
|
||||
customizationId={this.customizationId}
|
||||
/>
|
||||
);
|
||||
}
|
||||
},
|
||||
styles
|
||||
);
|
||||
|
||||
|
||||
|
||||
class ToobPlayerViewFactory implements IControlViewFactory {
|
||||
uri: string = "http://two-play.com/plugins/toob-player";
|
||||
|
||||
Create(model: PiPedalModel, pedalboardItem: PedalboardItem): React.ReactNode {
|
||||
return (<ToobPlayerView instanceId={pedalboardItem.instanceId} item={pedalboardItem} />);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
export default ToobPlayerViewFactory;
|
||||
@@ -126,8 +126,11 @@ const ToobPowerstage2View =
|
||||
this.isControlMounted = false;
|
||||
this.maybeListenForAtomOutput();
|
||||
}
|
||||
fullScreen() {
|
||||
return false;
|
||||
}
|
||||
|
||||
ModifyControls(controls: (React.ReactNode| ControlGroup)[]): (React.ReactNode| ControlGroup)[]
|
||||
modifyControls(controls: (React.ReactNode| ControlGroup)[]): (React.ReactNode| ControlGroup)[]
|
||||
{
|
||||
let time = Date.now()*0.001;
|
||||
|
||||
|
||||
@@ -61,8 +61,11 @@ const ToobSpectrumAnalyzerView =
|
||||
this.state = {
|
||||
}
|
||||
}
|
||||
fullScreen() {
|
||||
return false;
|
||||
}
|
||||
|
||||
ModifyControls(controls: (React.ReactNode| ControlGroup)[]): (React.ReactNode| ControlGroup)[]
|
||||
modifyControls(controls: (React.ReactNode| ControlGroup)[]): (React.ReactNode| ControlGroup)[]
|
||||
{
|
||||
controls.splice(0,0,
|
||||
( <ToobSpectrumResponseView instanceId={this.props.instanceId} />)
|
||||
|
||||
@@ -86,8 +86,11 @@ const ToobToneStackView =
|
||||
this.controlValueChangedHandle = undefined;
|
||||
}
|
||||
}
|
||||
fullScreen() {
|
||||
return false;
|
||||
}
|
||||
|
||||
ModifyControls(controls: (React.ReactNode| ControlGroup)[]): (React.ReactNode| ControlGroup)[]
|
||||
modifyControls(controls: (React.ReactNode| ControlGroup)[]): (React.ReactNode| ControlGroup)[]
|
||||
{
|
||||
if (this.state.isBaxandall)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* 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 { useState, useEffect, useCallback } from 'react';
|
||||
|
||||
const getSize = () => {
|
||||
return {
|
||||
width: window.innerWidth,
|
||||
height: window.innerHeight,
|
||||
};
|
||||
};
|
||||
|
||||
export interface WindowSize {
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
export default function useWindowSize() {
|
||||
|
||||
const [size, setSize] = useState<WindowSize>(getSize());
|
||||
|
||||
const handleResize = useCallback(() => {
|
||||
let ticking = false;
|
||||
if (!ticking) {
|
||||
window.requestAnimationFrame(() => {
|
||||
setSize(getSize());
|
||||
ticking = false;
|
||||
});
|
||||
ticking = true;
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener('resize', handleResize);
|
||||
return () => window.removeEventListener('resize', handleResize);
|
||||
}, []);
|
||||
|
||||
return [size];
|
||||
}
|
||||
Reference in New Issue
Block a user