modified: src/app/component/Bus.tsx

new file:   src/app/component/GainSlider.tsx
	modified:   src/app/component/Strip.tsx
	modified:   src/app/page.tsx
	new file:   src/utils/GetComponentState.ts
	new file:   src/utils/StopPropagation.ts
This commit is contained in:
Maksym 2024-10-16 18:19:10 +02:00
parent 81d889d66e
commit 3b2fb0264e
6 changed files with 250 additions and 126 deletions

View File

@ -13,6 +13,8 @@ import {
import React from "react"; import React from "react";
import { StripBusOutput, StripBusOutputEvent } from "./StripBusOutput"; import { StripBusOutput, StripBusOutputEvent } from "./StripBusOutput";
import { EventCounter } from "@/utils/EventCounter"; import { EventCounter } from "@/utils/EventCounter";
import { getComponentState } from "@/utils/GetComponentState";
import { GainSlider, GainSliderOnChangeEvent } from "./GainSlider";
export interface BusProps { export interface BusProps {
values?: Partial<Omit<BusState, StateDefaultValueException>>; values?: Partial<Omit<BusState, StateDefaultValueException>>;
@ -24,7 +26,6 @@ export interface BusProps {
type StateDefaultValueException = ""; type StateDefaultValueException = "";
export interface BusState { export interface BusState {
gain: number;
muted: boolean; muted: boolean;
selected: boolean; selected: boolean;
} }
@ -45,56 +46,14 @@ export interface BusSelectToggle {
} }
export class Bus extends React.Component<BusProps, BusState> { export class Bus extends React.Component<BusProps, BusState> {
private eventCounter = new EventCounter<"onSliderResetDefaults">();
private defaultValues: Omit<BusState, StateDefaultValueException> = { private defaultValues: Omit<BusState, StateDefaultValueException> = {
gain: 0,
selected: false, selected: false,
muted: false, muted: false,
}; };
slider = styled(Slider)(({ theme }) => ({
"& .MuiSlider-valueLabel": {
top: 0,
fontFamily: '"Roboto","Helvetica","Arial",sans-serif',
fontWeight: 400,
fontSize: "0.75rem",
width:"4em",
letterSpacing: "0.03333em",
backgroundColor: "unset",
color: theme.palette.text.primary,
"&::before": {
display: "none",
},
"& *": {
background: "transparent",
color: theme.palette.mode === "dark" ? "#fff" : "#000",
},
},
}));
constructor(props: BusProps) { constructor(props: BusProps) {
super(props); super(props);
let { values: initialValues } = props; let { values: initialValues } = props;
const getValue = (name: keyof typeof this.defaultValues) => { this.state = getComponentState<BusState, "">(props.values, this.defaultValues);
if (initialValues === undefined) return this.defaultValues[name];
return initialValues[name] !== undefined
? this.defaultValues[name]
: initialValues[name];
};
this.state = {
...(Object.fromEntries(
Object.entries(this.defaultValues).map(([name]) => [
name,
getValue(name as keyof typeof this.defaultValues) as any,
])
) as Omit<BusState, StateDefaultValueException>),
};
this.eventCounter.on({
callback: this.onResetDefaults.bind(this),
count: 2,
name: "onSliderResetDefaults",
});
}
onResetDefaults() {
this.setState({ gain: this.defaultValues.gain });
} }
onKeyDown(event: React.KeyboardEvent) { onKeyDown(event: React.KeyboardEvent) {
console.log(event); console.log(event);
@ -103,24 +62,13 @@ export class Bus extends React.Component<BusProps, BusState> {
event.preventDefault(); event.preventDefault();
} }
} }
onGainChange(event: Event | React.ChangeEvent<HTMLInputElement>) { onGainSliderChange(event: GainSliderOnChangeEvent) {
let changedGain = parseFloat((event.target as HTMLInputElement).value); const { gain, type } = event;
if (changedGain > 12) changedGain = 12; if (type === "GainChanged")
else if (changedGain < -60) changedGain = -60; this.props.onChange({
this.setState({ type: "StripGainChanged",
gain: changedGain, gain,
}); });
this.props.onChange({
type: "StripGainChanged",
gain: changedGain,
});
}
onStripWheel(event: React.WheelEvent) {
console.log(event);
}
onSliderClick(event: React.MouseEvent) {
this.stopPropagation(event);
this.eventCounter.emit("onSliderResetDefaults");
} }
onMuteToggle(event: React.MouseEvent) { onMuteToggle(event: React.MouseEvent) {
this.stopPropagation(event); this.stopPropagation(event);
@ -156,26 +104,36 @@ export class Bus extends React.Component<BusProps, BusState> {
spacing={0} spacing={0}
sx={{ width: "inherit" }} sx={{ width: "inherit" }}
> >
{this.props.width > 600 ? ( {/* {this.props.width > 600 ? (
<TextField // <TextField
// inputProps={{ // inputProps={{
// "aria-labelledby": "input-slider", // "aria-labelledby": "input-slider",
// itemType: "number", // itemType: "number",
// style:{padding: 0} // style:{padding: 0}
// }} // }}
value={this.state.gain} // value={Bus.percentToGain(this.state.gainPercent).toFixed(1)}
onWheel={(e) => this.onStripWheel(e)} // onWheel={(e) => this.onStripWheel(e)}
onClick={(e) => this.stopPropagation(e)} // onClick={(e) => this.stopPropagation(e)}
onChange={(e) => this.onGainChange(e as unknown as Event)} // onChange={(e) => {
size="small" // e.target.value = Bus.gainToPercent(
label={this.props.name} // parseInt(e.target.value)
type="number" // ).toFixed(0);
variant="outlined" // this.onGainSliderChange(e as unknown as Event);
sx={{ minWidth: "100px", maxWidth: "100px" }} // }}
/> // size="small"
// label={this.props.name}
// type="number"
// variant="outlined"
// sx={{
// minWidth: "100px",
// maxWidth: "100px",
// height: "",
// marginBlockEnd: "8px",
// }}
// />
) : ( ) : (
<Typography variant="caption">{this.props.name}</Typography> <Typography variant="caption">{this.props.name}</Typography>
)} )} */}
{/* {this.props.width <= 600 && ( {/* {this.props.width <= 600 && (
<Typography <Typography
@ -189,27 +147,11 @@ export class Bus extends React.Component<BusProps, BusState> {
{this.state.gain}dB {this.state.gain}dB
</Typography> </Typography>
)} */} )} */}
<this.slider <GainSlider
sx={{ sliderProps={{
// margin: "5px", valueLabelDisplay: this.props.width > 600 ? "off" : "on",
marginInline: "10px",
}} }}
valueLabelFormat={(e) => `${e} dB`} onChange={(e) => this.onGainSliderChange(e)}
// orientation="vertical"
color={
(this.state.gain > 0 && this.state.gain < 12 && "warning") ||
(this.state.gain >= 12 && "error") ||
(this.state.gain < 0 && "info") ||
"primary"
}
defaultValue={0.0}
step={0.1}
min={-60}
max={12}
value={this.state.gain}
valueLabelDisplay={this.props.width > 600 ? "off" : "on"}
onChange={(e) => this.onGainChange(e)}
onClick={(e) => this.onSliderClick(e)}
/> />
<ButtonGroup> <ButtonGroup>

View File

@ -0,0 +1,153 @@
import { EventCounter } from "@/utils/EventCounter";
import { getComponentState } from "@/utils/GetComponentState";
import { stopPropagation } from "@/utils/StopPropagation";
import { Slider, SliderOwnProps, styled } from "@mui/material";
import React from "react";
export type GainSliderOnChangeEventType = "GainChanged";
export interface GainSliderOnChangeEvent {
type: GainSliderOnChangeEventType;
gain: number;
}
export interface GainSliderOwnProps {
gain?: number;
onChange: (event: GainSliderOnChangeEvent) => any;
}
export interface GainSliderOwnState {
gainPercent: number;
}
export type GainSliderProps = GainSliderOwnProps& {sliderProps?: SliderOwnProps}
export const SliderStyled = styled(Slider)(({ theme }) => ({
"& .MuiSlider-thumb": {
width: "25px",
height: "25px",
},
"& .MuiSlider-valueLabel": {
top: 10,
left: -10,
fontFamily: '"Roboto","Helvetica","Arial",sans-serif',
fontWeight: 400,
fontSize: "0.75rem",
width: "4em",
letterSpacing: "0.03333em",
backgroundColor: "unset",
color: theme.palette.text.primary,
"&::before": {
display: "none",
},
"& *": {
background: "transparent",
color: theme.palette.mode === "dark" ? "#000" : "#fff",
},
},
}));
export class GainSlider extends React.Component<
GainSliderProps,
GainSliderOwnState
> {
/*[fromRangeMin, fromRangeMax],
[toRangeMin, toRangeMax]*/
private static o = {
//originally gain to percent
lessThanHalf: [
[-60, 0],
[0, 50],
],
halfOrMore: [
[0, 12],
[50, 100],
],
};
static gainToPercent(gain: number) {
const key: keyof typeof GainSlider.o =
gain >= 0 ? "halfOrMore" : "lessThanHalf";
return this.convertRange(gain, ...GainSlider.o[key]);
}
static percentToGain(percent: number) {
const key: keyof typeof GainSlider.o =
percent >= 50 ? "halfOrMore" : "lessThanHalf";
return this.convertRange(
percent,
...GainSlider.o[key].map((i) => i.reverse()).reverse()
);
}
static convertRange(value: number, ...ranges: number[][]) {
const [output, input] = ranges;
return (
((value - output[0]) * (input[1] - input[0])) / (output[1] - output[0]) +
input[0]
);
}
static stopPropagation(event: Event | React.MouseEvent) {
if (event instanceof Event) event.stopImmediatePropagation();
else event.nativeEvent.stopImmediatePropagation();
}
private eventCounter = new EventCounter<"SliderClick">();
defaultValues: GainSliderOwnState = { gainPercent: 50 };
constructor(props: GainSliderProps) {
super(props);
this.state = getComponentState<GainSliderOwnState, "gainPercent">(
props,
this.defaultValues
);
this.eventCounter.on({
name: "SliderClick",
count: 2,
callback: this.resetGain.bind(this),
});
}
resetGain() {
this.setState({ gainPercent: this.defaultValues.gainPercent });
}
onGainChange(event: Event): void {
let changedGain = parseFloat((event.target as HTMLInputElement).value);
if (changedGain > 100) changedGain = 100;
else if (changedGain < 0) changedGain = 0;
this.setState({
gainPercent: changedGain,
});
this.props.onChange({
type: "GainChanged",
gain: Math.round(GainSlider.percentToGain(changedGain) * 10) / 10,
});
}
onSliderClick(e: React.MouseEvent<HTMLSpanElement, MouseEvent>): void {
stopPropagation(e);
this.eventCounter.emit("SliderClick");
}
render(): React.ReactNode {
return (
<SliderStyled
sx={{
// margin: "5px",
marginInline: "10px",
}}
valueLabelFormat={(e) => `${e.toFixed(1)} dB`}
// orientation="vertical"
color={
(this.state.gainPercent < GainSlider.gainToPercent(0) && "info") ||
(this.state.gainPercent > GainSlider.gainToPercent(0) &&
this.state.gainPercent < 100 &&
"warning") ||
(this.state.gainPercent === 100 && "error") ||
"primary"
}
step={1}
min={0}
max={100}
value={this.state.gainPercent}
onChange={(e) => this.onGainChange(e)}
onClick={(e) => this.onSliderClick(e)}
scale={(v) => GainSlider.percentToGain(v)}
{...this.props.sliderProps}
/>
);
}
}

View File

@ -7,12 +7,14 @@ import {
Input, Input,
Slider, Slider,
Stack, Stack,
styled,
TextField, TextField,
Typography, Typography,
} from "@mui/material"; } from "@mui/material";
import React from "react"; import React from "react";
import { StripBusOutput, StripBusOutputEvent } from "./StripBusOutput"; import { StripBusOutput, StripBusOutputEvent } from "./StripBusOutput";
import { EventCounter } from "@/utils/EventCounter"; import { EventCounter } from "@/utils/EventCounter";
import { stopPropagation } from "@/utils/StopPropagation";
export interface StripProps { export interface StripProps {
physicalBuses: number; physicalBuses: number;
@ -83,7 +85,7 @@ export class Strip extends React.Component<StripProps, StripState> {
this.setState({ gain: this.defaultValues.gain }); this.setState({ gain: this.defaultValues.gain });
} }
onSliderClick(event: React.MouseEvent) { onSliderClick(event: React.MouseEvent) {
this.stopPropagation(event); stopPropagation(event);
this.eventCounter.emit("onSliderResetDefaults"); this.eventCounter.emit("onSliderResetDefaults");
} }
onKeyDown(event: React.KeyboardEvent) { onKeyDown(event: React.KeyboardEvent) {
@ -94,7 +96,7 @@ export class Strip extends React.Component<StripProps, StripState> {
} }
} }
onGainChange(event: Event) { onGainChange(event: Event) {
this.stopPropagation(event); stopPropagation(event);
const multiplier = (event as unknown as React.KeyboardEvent).shiftKey const multiplier = (event as unknown as React.KeyboardEvent).shiftKey
? 2 ? 2
: 1; : 1;
@ -127,7 +129,7 @@ export class Strip extends React.Component<StripProps, StripState> {
console.log(event); console.log(event);
} }
onMuteToggle(event: React.MouseEvent) { onMuteToggle(event: React.MouseEvent) {
this.stopPropagation(event); stopPropagation(event);
this.setState( this.setState(
(state) => ({ muted: !state.muted }), (state) => ({ muted: !state.muted }),
() => { () => {
@ -135,10 +137,7 @@ export class Strip extends React.Component<StripProps, StripState> {
} }
); );
} }
stopPropagation(event: Event | React.MouseEvent) {
if (event instanceof Event) event.stopImmediatePropagation();
else event.nativeEvent.stopImmediatePropagation();
}
render() { render() {
return ( return (
<> <>
@ -158,7 +157,7 @@ export class Strip extends React.Component<StripProps, StripState> {
// }} // }}
value={this.state.gain} value={this.state.gain}
onWheel={(e) => this.onStripWheel(e)} onWheel={(e) => this.onStripWheel(e)}
onClick={(e) => this.stopPropagation(e)} onClick={(e) => stopPropagation(e)}
size="small" size="small"
type="number" type="number"
label={this.props.name} label={this.props.name}
@ -167,32 +166,20 @@ export class Strip extends React.Component<StripProps, StripState> {
/> />
) : ( ) : (
<> <>
<Typography variant="caption">{this.props.name}</Typography> {/* <Typography variant="caption">{this.props.name}</Typography>
<Typography width={"4em"} textAlign="center" variant="caption"> <Typography width={"4em"} textAlign="center" variant="caption">
{this.state.gain} dB {this.state.gain} dB
</Typography> </Typography> */}
</> </>
)} )}
<Slider <Slider
sx={{
// margin: "5px",
marginBlockStart: "10px",
}}
color={
(this.state.gain > 0 && this.state.gain < 12 && "warning") ||
(this.state.gain >= 12 && "error") ||
(this.state.gain < 0 && "info") ||
"primary"
}
orientation="vertical" orientation="vertical"
defaultValue={0.0} slotProps={{
step={0.1} root: {
min={-60} onClick: (e) => this.onSliderClick(e),
max={12} },
value={this.state.gain} }}
aria-label="Temperature"
onChange={(e) => this.onGainChange(e)} onChange={(e) => this.onGainChange(e)}
onClick={(e) => this.onSliderClick(e)}
/> />
</Stack> </Stack>
<Stack> <Stack>

View File

@ -1,5 +1,5 @@
"use client"; "use client";
import React, { useEffect, useMemo, useState } from "react"; import React, { createRef, useEffect, useMemo, useState } from "react";
import { Strip, StripEvent } from "./component/Strip"; import { Strip, StripEvent } from "./component/Strip";
import { import {
Button, Button,
@ -33,8 +33,21 @@ export default function Home() {
}), }),
[] []
); );
function onStripEvent(event: StripEvent) { function sendApi(path: string, fetchOptions: RequestInit) {
const url = new URL(path, "http://127.0.0.1:3000/");
return fetch(url.toString(), fetchOptions).then((r) => r.json());
}
function onStripEvent(event: StripEvent, stripId: number) {
console.log(event); console.log(event);
switch (event.type) {
case "StripMuted": {
sendApi(`/strip/${stripId}/mute`, {
method: "PATCH",
body: JSON.stringify({ muteState: event.muted }),
});
break;
}
}
} }
const isSnackBarHidden = useMemo(() => { const isSnackBarHidden = useMemo(() => {
if (typeof localStorage === "undefined") return false; if (typeof localStorage === "undefined") return false;
@ -114,7 +127,7 @@ export default function Home() {
}} }}
color="secondary" color="secondary"
> >
Gotcha OK
</Button> </Button>
</> </>
); );
@ -155,7 +168,7 @@ export default function Home() {
stripId + 1 stripId + 1
}`}</Typography> */} }`}</Typography> */}
<Strip <Strip
onChange={(ev) => onStripEvent(ev)} onChange={(ev) => onStripEvent(ev, stripId)}
physicalBuses={buses.physical} physicalBuses={buses.physical}
virtualBuses={buses.virtual} virtualBuses={buses.virtual}
width={width} width={width}

View File

@ -0,0 +1,25 @@
export function getComponentState<
State extends Object,
StateDefaultValueException extends keyof State | ""
>(
initialValues: Partial<Omit<State, StateDefaultValueException>> | undefined,
defaultValues: State
) {
type InitialValuesType = Omit<State, StateDefaultValueException>;
// const { initialValues } = props;
const getValue = (name: keyof InitialValuesType) => {
if (initialValues === undefined) return defaultValues[name];
return initialValues[name] !== undefined
? defaultValues[name]
: initialValues[name];
};
const state = {
...(Object.fromEntries(
Object.entries(defaultValues).map(([name]) => [
name,
getValue(name as keyof InitialValuesType) as any,
])
) as InitialValuesType),
} as State;
return state;
}

View File

@ -0,0 +1,4 @@
export function stopPropagation(event: Event | React.MouseEvent) {
if (event instanceof Event) event.stopImmediatePropagation();
else event.nativeEvent.stopImmediatePropagation();
}