Files
op-pedal/vite/src/pipedal/ToobPlayerControl.tsx
T
Robin E. R. Davies 890368352f MOD Gui support.
2025-07-10 11:37:02 -04:00

907 lines
32 KiB
TypeScript

/*
* 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 LoopDialog from './LoopDialog';
import Box from '@mui/material/Box';
import Typography from '@mui/material/Typography';
import ButtonEx from './ButtonEx';
import IconButtonEx from './IconButtonEx';
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, State } from './PiPedalModel';
import { pathFileNameOnly } from './FileUtils';
import ButtonBase from '@mui/material/ButtonBase';
import FilePropertyDialog from './FilePropertyDialog';
import JsonAtom from './JsonAtom';
import { UiFileProperty } from './Lv2Plugin';
import Divider from '@mui/material/Divider';
import useWindowSize from './UseWindowSize';
import { getAlbumArtUri } from './AudioFileMetadata';
import RepeatIcon from '@mui/icons-material/Repeat';
import Timebase, { LoopParameters, TimebaseUnits } from './Timebase';
import ControlSlider from './ControlSlider';
import { getTrackTitle } from './AudioFileMetadata';
let Player__seek = "http://two-play.com/plugins/toob-player#seek"
const AUDIO_FILE_PROPERTY_URI = "http://two-play.com/plugins/toob-player#audioFile";
const LOOP_PROPERTY_URI = "http://two-play.com/plugins/toob-player#loop";
class PluginState {
// Must match ProcessorState values in Lv2AudioFileProcessor.hpp
// Not an enum because it has to be json serializable.
static Idle = 0;
static Recording = 1;
static StoppingRecording = 2;
static CuePlayingThenPlay = 3;
static CuePlayingThenPause = 4;
static Paused = 5;
static Playing = 6;
static Error = 7;
};
const useWallpaper = false;
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 tidyHundredths(value: number): string {
if (value === 0) {
return "";
} else if (value % 10 == 0) {
return `.${(value / 10).toString()}`;
} else {
return `.${value.toString()}`;
}
}
export function formatTimeCompact(timebase: Timebase, sampleRate: number, seconds: number): string {
switch (timebase.units) {
case TimebaseUnits.Samples:
{
return Math.round(seconds * sampleRate).toString();
}
break;
case TimebaseUnits.Seconds:
{
const t = Math.round(seconds * 100);
let hundredths = t % 100;
let secs = Math.floor(t / 100);
let minutes = Math.floor(secs / 60);
let hours = Math.floor(minutes / 60);
minutes = minutes % 60;
secs = secs % 60;
if (hours == 0) {
return `${minutes}:${secs.toString().padStart(2, '0')}${tidyHundredths(hundredths)}`;
} else {
return `${hours}:${minutes}:${secs.toString().padStart(2, '0')}${tidyHundredths(hundredths)}`;
}
break;
}
case TimebaseUnits.Beats:
{
let t = Math.round(seconds * timebase.tempo / 60.0 * 100.0);
let hundredths = t % 100;
let beats = Math.floor(t / 100.0);
let bars = Math.floor(beats / timebase.timeSignature.numerator);
let beat = (beats - bars * timebase.timeSignature.numerator);
return `${bars + 1}:${(beat + 1)}${tidyHundredths(hundredths)}`;
}
break;
default:
throw new Error("Unsupported timebase units");
}
}
function timebaseEqual(a: Timebase, b: Timebase): boolean {
if (a.units !== b.units) {
return false;
}
if (a.tempo !== b.tempo) {
return false;
}
if (a.timeSignature.numerator !== b.timeSignature.numerator) {
return false;
}
if (a.timeSignature.denominator !== b.timeSignature.denominator) {
return false;
}
return true;
}
function getAlbumLine(album: string, artist: string, albumArtist: string): string {
if (artist === "") {
artist = albumArtist;
}
let joiner = (artist !== "" && album !== "") ? " - " : "";
return album + joiner + artist;
}
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 4px 12px rgba(0,0,0,0.2)",
...theme.applyStyles('dark', {
backgroundColor: useWallpaper ? 'rgba(0,0,0,0.6)' : '#282828',
}),
}));
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: useWallpaper ? 'rgba(0,0,0,0.6)' : theme.mainBackground,
}),
}));
// const WidgetNoWallpaper = styled('div')(({ theme }) => ({
// padding: 16,
// borderRadius: 0,
// width: "100%",
// height: "100%",
// margin: 0,
// position: 'relative',
// zIndex: 1,
// }));
function Widget(props: WidgetProps) {
// if (!useWallpaper) {
// return(<WidgetNoWallpaper>{props.children} </WidgetNoWallpaper>);
// }
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%',
},
});
export interface ToobPlayerControlProps {
instanceId: number;
extraControls: React.ReactElement[];
}
export default function ToobPlayerControl(
props: ToobPlayerControlProps
) {
const model = PiPedalModelFactory.getInstance();
const sampleRate = model.jackConfiguration.get().sampleRate == 0 ?
48000 :
model.jackConfiguration.get().sampleRate;
const defaultCoverArt = "/img/default_album.jpg";
const [serverConnected, setServerConnected] = React.useState(model.state.get() == State.Ready);
const [duration, setDuration] = React.useState(0.0);
const [start, setStart] = React.useState(0.0);
const [loopStart, setLoopStart] = React.useState(0.0);
const [loopEnable, setLoopEnable] = React.useState(false);
const [loopEnd, setLoopEnd] = React.useState(0.0);
const [pluginState, setPluginState] = React.useState(0.0);
const [coverArt, setCoverArt] = React.useState(defaultCoverArt);
const [audioFile, setAudioFile] = React.useState("");
const [position, setPosition] = React.useState(0.0);
//let position = 0;
const [title, setTitle] = React.useState("");
const [album, setAlbum] = React.useState("");
const [artist, setArtist] = React.useState("");
const [albumArtist, setAlbumArtist] = React.useState("");
const [showFileDialog, setShowFileDialog] = React.useState(false);
const [showLoopDialog, setShowLoopDialog] = React.useState(false);
const [timebase, setTimebase] = React.useState<Timebase>(
{
units: TimebaseUnits.Seconds,
tempo: 120.0,
timeSignature: { numerator: 4, denominator: 4 }
});
const [loopParameters, setLoopParameters] = React.useState<LoopParameters>({
start: 0.0,
loopEnable: false,
loopStart: 0.0,
loopEnd: 0.0
});
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 noBorders = width < 420 || height < 720;
let useQuadMixPanel = width < 720;
if (noBorders) {
useQuadMixPanel = width < 370;
}
function SelectFile() {
setShowFileDialog(true);
}
function onAudioFileChanged(path: string) {
setAudioFile(path);
if (path === "") {
setTitle("");
setAlbum("");
setArtist("");
setAlbumArtist("");
setCoverArt(defaultCoverArt);
return;
}
model.getAudioFileMetadata(path)
.then((metadata) => {
let coverArtUri = getAlbumArtUri(model, metadata, path);
setCoverArt(coverArtUri);
setTitle(getTrackTitle(path, metadata));
setAlbum(metadata.album);
setArtist(metadata.artist);
setAlbumArtist(metadata.albumArtist);
})
.catch((e) => {
setTitle("#error" + e.message);
setAlbum("");
});
}
function ControlCluster() {
return (
<Box
sx={(theme) => ({
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
mt: -1,
'& svg': {
color: '#000',
...theme.applyStyles('dark', {
color: '#fff',
}),
},
})}
>
<IconButtonEx tooltip="Previous track"
style={{ opacity: (pluginState === PluginState.Idle ? 0.2 : 0.7) }}
onClick={() => {
if (position > start + 3.0) {
model.sendPedalboardControlTrigger(
props.instanceId,
"stop",
1
);
} else {
onPreviousTrack();
}
}}
>
<FastRewind fontSize="large" />
</IconButtonEx>
<IconButtonEx
tooltip={paused ? "Play" : "Pause"}
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' }} />
)}
</IconButtonEx>
<IconButtonEx tooltip="Next track"
style={{ opacity: (pluginState == PluginState.Idle ? 0.2 : 0.7) }}
onClick={() => { onNextTrack(); }}
>
<FastForward fontSize="large" />
</IconButtonEx>
</Box>
);
}
function FilePanel() {
let textColor = pluginState == PluginState.Error ? "error" : "textPrimary";
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 }}
onDragStart={(e) => {
e.preventDefault();
}}
alt="Cover Art"
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" color={textColor} noWrap>
{titleLine}
</Typography>
<Typography variant="body2" color={textColor} noWrap sx={{}}>
{albumLine}
</Typography>
</div>
)}
</Box>
</Box>
</ButtonBase>
);
}
function loopButtonText() {
if (start === 0 && !loopEnable) {
return "Set loop";
} else if (loopEnable) {
return `${formatTimeCompact(timebase, sampleRate, start)} [${formatTimeCompact(timebase, sampleRate, loopStart)} - ${formatTimeCompact(timebase, sampleRate, loopEnd)}]`;
} else {
return `Start: ${formatTimeCompact(timebase, sampleRate, start)}`;
}
}
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 onLoopPropertyChanged(loopSettingsJson: string) {
try {
if (loopSettingsJson === "") {
setTimebase(
{
units: TimebaseUnits.Seconds,
tempo: 120.0,
timeSignature: { numerator: 4, denominator: 4 }
})
let loopParameters = {
start: 0.0,
loopEnable: false,
loopStart: 0.0,
loopEnd: 0.0
};
setLoopParameters(loopParameters);
setStart(0.0);
setLoopEnable(false);
setLoopStart(0.0);
setLoopEnd(0.0);
return;
}
let atomObject = JSON.parse(loopSettingsJson);
let loopParameters: LoopParameters = atomObject.loopParameters as LoopParameters;
let newTimebase: Timebase | undefined = atomObject.timebase as (Timebase | undefined);;
if (newTimebase !== undefined) {
if (!timebaseEqual(timebase, newTimebase)) {
setTimebase(newTimebase);
}
setLoopParameters(loopParameters);
setLoopEnable(loopParameters.loopEnable);
setStart(loopParameters.start);
setLoopStart(loopParameters.loopStart);
setLoopEnd(loopParameters.loopEnd);
} else {
throw new Error("Invalid loop settings.");
}
} catch (e) {
console.warn("Unable to parse loop settings.");
setTimebase(
{
units: TimebaseUnits.Seconds,
tempo: 120.0,
timeSignature: { numerator: 4, denominator: 4 }
})
setLoopEnable(false);
setStart(0.0);
setLoopStart(0.0);
setLoopEnd(0.0);
}
}
function onNextTrack() {
model.getNextAudioFile(audioFile)
.then((file) => {
let json = JsonAtom._Path(file).asAny();
model.setPatchProperty(
props.instanceId,
AUDIO_FILE_PROPERTY_URI,
json);
})
.catch(() => {
});
}
function onPreviousTrack() {
model.getPreviousAudioFile(audioFile)
.then((file) => {
let json = JsonAtom._Path(file).asAny();
model.setPatchProperty(
props.instanceId,
AUDIO_FILE_PROPERTY_URI,
json);
})
.catch(() => {
});
}
function OnSeek(value: number) {
model.setPatchProperty(props.instanceId, Player__seek, value)
.then(() => {
}).catch((e) => {
console.warn("Seek error. " + e.toString());
});
}
function onStateChanged(value: State) {
setServerConnected(value === State.Ready);
}
useEffect(() => {
model.state.addOnChangedHandler(onStateChanged);
if (model.state.get() !== State.Ready) {
// wait for it.
return () => {
model.state.removeOnChangedHandler(onStateChanged);
}
}
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);
}
}
);
let loopPropertyHandle = model.monitorPatchProperty(
props.instanceId,
LOOP_PROPERTY_URI,
(instanceId: number, propertyUri: string, atomObject: any) => {
if (typeof (atomObject) === "string") {
onLoopPropertyChanged(atomObject as string);
}
}
);
model.getPatchProperty(props.instanceId, AUDIO_FILE_PROPERTY_URI)
.then((o) => {
let path = o.value;
onAudioFileChanged(path);
});
model.getPatchProperty(props.instanceId, LOOP_PROPERTY_URI)
.then((o) => {
onLoopPropertyChanged(o as string);
})
return () => {
model.state.removeOnChangedHandler(onStateChanged);
model.unmonitorPort(durationHandle);
model.unmonitorPort(pluginStateHandle);
model.unmonitorPort(positionHandle);
// model.unmonitorPort(loopEnableHandle);
// model.unmonitorPort(startHandle);
// model.unmonitorPort(loopStartHandle);
// model.unmonitorPort(loopEndHandle);
model.cancelMonitorPatchProperty(filePropertyHandle);
model.cancelMonitorPatchProperty(loopPropertyHandle);
};
},
[serverConnected]
);
const titleLine = title !== "" ? title : pathFileNameOnly(audioFile);
const albumLine = getAlbumLine(album, artist, albumArtist);
const paused = (pluginState === PluginState.Idle
|| pluginState === PluginState.CuePlayingThenPause
|| pluginState === PluginState.Paused
|| pluginState === PluginState.Error);
function handlePreview() {
if (paused) {
model.sendPedalboardControlTrigger(
props.instanceId,
"play",
1
);
} else {
model.sendPedalboardControlTrigger(
props.instanceId,
"stop",
1
);
}
}
function handleCancelPlaying() {
if (!paused) {
model.sendPedalboardControlTrigger(
props.instanceId,
"stop",
1
);
}
}
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={{ position: "relative", display: "flex", flexFlow: "column nowrap" }}>
<div style={{ marginBottom: 16 }}>
<ControlSlider
instanceId={props.instanceId}
controlKey="position"
duration={duration}
onPreviewValue={(value) => {
}}
onValueChanged={(value) => {
OnSeek(value);
}}
/>
</div>
<div
style={{
position: "absolute",
width: "100%",
display: "flex", flexFlow: "row nowrap", alignItems: "center",
justifyContent: "center",
bottom: 0
}}
>
<ButtonEx tooltip="Loop Settings" variant="dialogSecondary"
style={{
flex: "0 1 auto", maxWidth: 200,
textTransform: "none", padding: "4px 8px"
}}
disabled={audioFile === ""}
startIcon={(<RepeatIcon />)}
onClick={() => {
if (audioFile !== "") {
setShowLoopDialog(true);
}
}}
>
<Typography noWrap variant="caption">
{loopButtonText()
}
</Typography>
</ButtonEx>
</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()
}
{SliderCluster()}
{
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',
}}>
{useWallpaper && (<WallPaper />)}
{
useHorizontalLayout ?
HorizontalWidget() : VerticalWidget()
}
{showLoopDialog && (
<LoopDialog isOpen={showLoopDialog}
sampleRate={
sampleRate
}
playing={!paused}
onPreview={() => { handlePreview(); }}
value={loopParameters}
onCancelPlaying={() => { handleCancelPlaying(); }}
timebase={timebase}
onTimebaseChange={(newTimebase) => {
setTimebase(newTimebase);
// model.setPatchProperty(
// props.instanceId,
// "timebase",
// newTimebase
// );
}}
onClose={() => {
setShowLoopDialog(false);
}}
onSetLoop={(loop: LoopParameters) => {
let loopSettings = {
timebase: timebase,
loopParameters: loop
};
model.setPatchProperty(
props.instanceId,
LOOP_PROPERTY_URI,
JSON.stringify(loopSettings));
setStart(loop.start);
setLoopEnable(loop.loopEnable);
setLoopStart(loop.loopStart);
setLoopEnd(loop.loopEnd);
}}
duration={duration}
/>
)}
{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).asAny()
)
.then(() => {
})
.catch((error) => {
model.showAlert("Unable to complete the operation. " + error);
});
}}
onOk={(fileProperty, selectedFile) => {
model.setPatchProperty(
props.instanceId,
fileProperty.patchProperty,
JsonAtom._Path(selectedFile).asAny()
)
.then(() => {
})
.catch((error) => {
model.showAlert("Unable to complete the operation. " + error);
});
setShowFileDialog(false);
}
}
/>
)}
</div>
);
}