Toob Player UI

This commit is contained in:
Robin E. R. Davies
2025-06-07 21:52:23 -04:00
parent 82f94cb152
commit 6ea45f46df
29 changed files with 1860 additions and 319 deletions
+192 -76
View File
@@ -23,17 +23,19 @@
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 Button from '@mui/material/Button';
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 { PiPedalModelFactory, State } from './PiPedalModel';
import ButtonTooltip from './ButtonTooltip';
import { pathFileNameOnly } from './FilePropertyDialog'
import { pathFileNameOnly } from './FileUtils';
import ButtonBase from '@mui/material/ButtonBase';
import FilePropertyDialog from './FilePropertyDialog';
import JsonAtom from './JsonAtom';
@@ -41,6 +43,8 @@ import { UiFileProperty } from './Lv2Plugin';
import { Divider } from '@mui/material';
import useWindowSize from './UseWindowSize';
import { getAlbumArtUri } from './AudioFileMetadata';
import RepeatIcon from '@mui/icons-material/Repeat';
import { LoopParameters, TimebaseUnits } from './Timebase';
let Player__seek = "http://two-play.com/plugins/toob-player#seek"
const AUDIO_FILE_PROPERTY_URI = "http://two-play.com/plugins/toob-player#audioFile";
@@ -55,6 +59,7 @@ class PluginState {
static Error = 6;
};
const useWallpaper = false;
const WallPaper = styled('div')({
position: 'absolute',
width: '100%',
@@ -87,6 +92,14 @@ const WallPaper = styled('div')({
},
});
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;
@@ -104,9 +117,9 @@ const WidgetBorders = styled('div')(({ theme }) => ({
position: 'relative',
zIndex: 1,
backgroundColor: 'rgba(255,255,255,0.4)',
boxShadow: "1px 3px 20px #0008",
boxShadow: "1px 4px 12px rgba(0,0,0,0.2)",
...theme.applyStyles('dark', {
backgroundColor: 'rgba(0,0,0,0.6)',
backgroundColor: useWallpaper? 'rgba(0,0,0,0.6)' : '#282828',
}),
}));
@@ -121,12 +134,26 @@ const WidgetNoBorders = styled('div')(({ theme }) => ({
zIndex: 1,
backgroundColor: 'rgba(255,255,255,0.4)',
...theme.applyStyles('dark', {
backgroundColor: 'rgba(0,0,0,0.6)',
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 {
@@ -163,9 +190,17 @@ export interface ToobPlayerControlProps {
export default function ToobPlayerControl(
props: ToobPlayerControlProps
) {
const model = PiPedalModelFactory.getInstance();
const defaultCoverArt = "/img/default_album.jpg";
const [serverConnected, setServerConnected] = React.useState(model.state.get() == State.Ready);
const [duration, setDuration] = React.useState(0.0);
const [position, setPosition] = 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 [dragging, setDragging] = React.useState(false);
const [pluginState, setPluginState] = React.useState(0.0);
const [sliderValue, setSliderValue] = React.useState(0.0);
@@ -173,9 +208,17 @@ export default function ToobPlayerControl(
const [audioFile, setAudioFile] = React.useState("");
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(
{
units: TimebaseUnits.Seconds,
tempo: 120.0,
timeSignature: { numerator: 4, denominator: 4 }
});
const model = PiPedalModelFactory.getInstance();
const [size] = useWindowSize();
const width = size.width;
@@ -185,8 +228,11 @@ export default function ToobPlayerControl(
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;
let useQuadMixPanel = width < 720;
if (noBorders) {
useQuadMixPanel = width < 370;
}
function SelectFile() {
setShowFileDialog(true);
@@ -202,6 +248,8 @@ export default function ToobPlayerControl(
if (path === "") {
setTitle("");
setAlbum("");
setArtist("");
setAlbumArtist("");
setCoverArt(defaultCoverArt);
return;
}
@@ -221,6 +269,8 @@ export default function ToobPlayerControl(
setCoverArt(coverArtUri);
setTitle(strTrack + metadata.title);
setAlbum(metadata.album);
setArtist(metadata.artist);
setAlbumArtist(metadata.albumArtist);
})
.catch((e)=>{
setTitle("#error" + e.message);
@@ -311,6 +361,10 @@ export default function ToobPlayerControl(
<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
}
@@ -324,10 +378,10 @@ export default function ToobPlayerControl(
) : (
<div>
<Typography variant="body1" noWrap>
{effectiveTitle}
{titleLine}
</Typography>
<Typography variant="body2" noWrap sx={{}}>
{album}
{albumLine}
</Typography>
</div>
@@ -338,6 +392,16 @@ export default function ToobPlayerControl(
);
}
function loopButtonText() {
if (start === 0 && !loopEnable) {
return "Set loop";
} else if (loopEnable) {
return `${formatDuration(start)} [${formatDuration(loopStart)} - ${formatDuration(loopEnd)}]`;
} else {
return `Start: ${formatDuration(start)}`;
}
}
function getUiFileProperty(uri: string): UiFileProperty {
let pedalboardItem = model.pedalboard.get().getItem(props.instanceId);
let uiPlugin = model.getUiPlugin(pedalboardItem.uri);
@@ -389,7 +453,17 @@ export default function ToobPlayerControl(
});
setSliderValue(value);
}
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
}
}
let durationHandle = model.monitorPort(props.instanceId, "duration", 1.0 / 15,
(value) => {
setDuration(value);
@@ -400,6 +474,27 @@ export default function ToobPlayerControl(
setPosition(value);
}
);
// let startHandle = model.monitorPort(props.instanceId, "start", 1.0,
// (value) => {
// setStart(value);
// }
// );
// let loopStartHandle = model.monitorPort(props.instanceId, "loopStart", 1.0,
// (value) => {
// setLoopStart(value);
// }
// );
// let loopEnableHandle = model.monitorPort(props.instanceId, "loopEnable", 1.0,
// (value) => {
// setLoopEnable(value != 0);
// }
// );
// let loopEndHandle = model.monitorPort(props.instanceId, "loopEnd", 1.0,
// (value) => {
// setLoopEnd(value);
// }
// );
let pluginStateHandle = model.monitorPort(props.instanceId, "state", 1.0 / 1000.0,
(value) => {
setPluginState(value);
@@ -428,17 +523,23 @@ export default function ToobPlayerControl(
return () => {
model.state.removeOnChangedHandler(onStateChanged);
model.unmonitorPort(durationHandle);
model.unmonitorPort(positionHandle);
model.unmonitorPort(pluginStateHandle);
// model.unmonitorPort(loopEnableHandle);
// model.unmonitorPort(startHandle);
// model.unmonitorPort(loopStartHandle);
// model.unmonitorPort(loopEndHandle);
model.cancelMonitorPatchProperty(filePropertyHandle);
};
},
[]
[serverConnected]
);
const effectivePosition =
(pluginState !== PluginState.Idle) ? Math.min(position, duration) : 0.0;
const effectiveTitle = title !== "" ? title : pathFileNameOnly(audioFile);
const titleLine = title !== "" ? title : pathFileNameOnly(audioFile);
const albumLine = getAlbumLine(album,artist,albumArtist);
const paused = (pluginState === PluginState.Idle
|| pluginState === PluginState.CuePlayPaused
@@ -529,12 +630,24 @@ export default function ToobPlayerControl(
flex: "0 0 auto",
width: "100%",
display: 'flex',
alignItems: 'center',
alignItems: 'top',
justifyContent: 'space-between',
marginTop: -4,
}}
>
<TinyText>{formatDuration(dragging ? sliderValue : effectivePosition)}</TinyText>
<Button title="Loop" variant="dialogSecondary"
style={{flex: "0 1 auto",width: 200,
textTransform: "none", fontSize: "0.9rem", fontWeight: 500, padding: "4px 8px"
}}
startIcon= {(<RepeatIcon />)}
onClick={() => {
setShowLoopDialog(true);
}}
>
{loopButtonText()
}
</Button>
<TinyText>{formatDuration(duration)}</TinyText>
</div>
</div>
@@ -554,69 +667,7 @@ export default function ToobPlayerControl(
{
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>
{SliderCluster()}
{
ControlCluster()
}
@@ -683,11 +734,76 @@ export default function ToobPlayerControl(
<div id="toobPlayerViewFrame" style={{
width: '100%', height: "100%", overflow: 'hidden', position: 'relative',
}}>
<WallPaper />
{useWallpaper && (<WallPaper />)}
{
useHorizontalLayout ?
HorizontalWidget() : VerticalWidget()
}
{showLoopDialog && (
<LoopDialog isOpen={showLoopDialog}
sampleRate={
model.jackConfiguration.get().sampleRate == 0?
96000 :
model.jackConfiguration.get().sampleRate
}
timebase={timebase}
onTimebaseChange={(newTimebase) => {
setTimebase(newTimebase);
// model.setPatchProperty(
// props.instanceId,
// "timebase",
// newTimebase
// );
}}
onClose={() => {
setShowLoopDialog(false);
}}
onSetLoop={(start, loopEnable,loopStart, loopEnd) => {
let loop: LoopParameters = { start: start,
loopEnable: loopEnable,
loopStart: loopStart,
loopEnd: loopEnd };
let loopSettings = {
timebase: timebase,
loopParameters: loop
};
model.setPatchProperty(
props.instanceId,
"http://two-play.com/plugins/toob-player#loopSettings",
loopSettings.toString());
setStart(start);
// model.setPedalboardControl(
// props.instanceId,
// "start",
// start);
setLoopEnable(loopEnable);
// model.setPedalboardControl(
// props.instanceId,
// "loopEnable",
// loopEnable? 1.0: 0.0);
setLoopStart(loopStart);
// model.setPedalboardControl(
// props.instanceId,
// "loopStart",
// loopStart);
setLoopEnd(loopEnd);
// model.setPedalboardControl(
// props.instanceId,
// "loopEnd",
// loopEnd
// );
}}
start={start}
loopStart={loopStart}
loopEnable={loopEnable}
loopEnd={loopEnd}
duration={duration}
/>
)}
{showFileDialog && (
<FilePropertyDialog open={showFileDialog}
fileProperty={getUiFileProperty(AUDIO_FILE_PROPERTY_URI)}