modified: package-lock.json

modified:   package.json
	modified:   src/app/component/Bus.tsx
	new file:   src/app/component/BusesList.tsx
	modified:   src/app/component/Strip.tsx
	modified:   src/app/component/StripBusOutput.tsx
	deleted:    src/app/globals.css
	modified:   src/app/page.tsx
	new file:   src/utils/DetectMobile.ts
	modified:   src/utils/EventCounter.ts
	new file:   src/utils/Range.ts
	modified:   tsconfig.json
This commit is contained in:
Maksym 2024-08-07 21:39:58 +02:00
parent 16b2de18a9
commit 7a65c76757
12 changed files with 310 additions and 198 deletions

7
package-lock.json generated
View File

@ -10,6 +10,7 @@
"dependencies": {
"@emotion/react": "^11.11.4",
"@emotion/styled": "^11.11.5",
"@fontsource/roboto": "^5.0.14",
"@mui/material": "^5.15.20",
"next": "14.2.4",
"next-pwa": "^5.6.0",
@ -1966,6 +1967,12 @@
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.2.tgz",
"integrity": "sha512-J4yDIIthosAsRZ5CPYP/jQvUAQtlZTTD/4suA08/FEnlxqW3sKS9iAhgsa9VYLZ6vDHn/ixJgIqRQPotoBjxIw=="
},
"node_modules/@fontsource/roboto": {
"version": "5.0.14",
"resolved": "https://registry.npmjs.org/@fontsource/roboto/-/roboto-5.0.14.tgz",
"integrity": "sha512-zHAxlTTm9RuRn9/StwclFJChf3z9+fBrOxC3fw71htjHP1BgXNISwRjdJtAKAmMe5S2BzgpnjkQR93P9EZYI/Q==",
"license": "Apache-2.0"
},
"node_modules/@humanwhocodes/config-array": {
"version": "0.11.14",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz",

View File

@ -11,6 +11,7 @@
"dependencies": {
"@emotion/react": "^11.11.4",
"@emotion/styled": "^11.11.5",
"@fontsource/roboto": "^5.0.14",
"@mui/material": "^5.15.20",
"next": "14.2.4",
"next-pwa": "^5.6.0",

View File

@ -2,6 +2,7 @@
import { Button, ButtonGroup, Grid, Input, Slider, Stack } from "@mui/material";
import React from "react";
import { StripBusOutput, StripBusOutputEvent } from "./StripBusOutput";
import { EventCounter } from "@/utils/EventCounter";
export interface BusProps {
values?: Partial<BusState>;
@ -30,28 +31,36 @@ export interface BusSelectToggle {
}
export class Bus extends React.Component<BusProps, BusState> {
private eventCounter = new EventCounter<"onSliderResetDefaults">();
private defaultValues: BusState = {
gain: 0,
selected: false,
muted: false,
};
constructor(props: BusProps) {
super(props);
const { values: initialValues } = props;
const defaultValues: BusState = {
gain: 0,
selected: false,
muted: false,
};
const getValue = (name: keyof typeof defaultValues) => {
if (initialValues === undefined) return defaultValues[name];
const getValue = (name: keyof typeof this.defaultValues) => {
if (initialValues === undefined) return this.defaultValues[name];
return initialValues[name] !== undefined
? defaultValues[name]
? this.defaultValues[name]
: initialValues[name];
};
this.state = Object.fromEntries(
Object.entries(defaultValues).map(([name]) => [
Object.entries(this.defaultValues).map(([name]) => [
name,
getValue(name as keyof typeof defaultValues) as any,
getValue(name as keyof typeof this.defaultValues) as any,
])
) as BusState;
this.eventCounter.on({
callback: this.onResetDefaults.bind(this),
count: 2,
name: "onSliderResetDefaults",
});
}
onResetDefaults() {
this.setState({ gain: this.defaultValues.gain });
}
onResetDefaults(e: React.MouseEvent) {}
onKeyDown(event: React.KeyboardEvent) {
console.log(event);
@ -59,12 +68,12 @@ export class Bus extends React.Component<BusProps, BusState> {
event.preventDefault();
}
}
onChange(event: React.ChangeEvent<HTMLInputElement>) {
const multiplier = (event as unknown as React.KeyboardEvent).shiftKey
? 2
: 1;
const changedGain =
parseFloat((event.target as HTMLInputElement).value) * multiplier;
onGainChange(event: Event | React.ChangeEvent<HTMLInputElement>) {
let changedGain =
parseFloat((event.target as HTMLInputElement).value);
if(changedGain > 12) changedGain = 12
else if(changedGain < -60) changedGain = -60;
this.setState({
gain: changedGain,
});
@ -76,7 +85,12 @@ export class Bus extends React.Component<BusProps, BusState> {
onStripWheel(event: React.WheelEvent) {
console.log(event);
}
onMuteToggle() {
onSliderClick(event: React.MouseEvent) {
this.stopPropagation(event);
this.eventCounter.emit("onSliderResetDefaults");
}
onMuteToggle(event: React.MouseEvent) {
this.stopPropagation(event);
this.setState(
(state) => ({ muted: !state.muted }),
() => {
@ -84,7 +98,8 @@ export class Bus extends React.Component<BusProps, BusState> {
}
);
}
onSelectToggle() {
onSelectToggle(event: React.MouseEvent) {
this.stopPropagation(event);
this.setState(
(state) => ({ selected: !state.selected }),
() => {
@ -95,6 +110,10 @@ export class Bus extends React.Component<BusProps, BusState> {
}
);
}
stopPropagation(event: Event | React.MouseEvent) {
if (event instanceof Event) event.stopImmediatePropagation();
else event.nativeEvent.stopImmediatePropagation();
}
render() {
return (
<>
@ -103,9 +122,12 @@ export class Bus extends React.Component<BusProps, BusState> {
inputProps={{
"aria-labelledby": "input-slider",
itemType: "number",
style:{padding: 0}
}}
value={this.state.gain}
onWheel={(ev) => this.onStripWheel(ev)}
onWheel={(e) => this.onStripWheel(e)}
onClick={(e) => this.stopPropagation(e)}
onChange={(e) => this.onGainChange(e as unknown as Event)}
size="small"
sx={{ width: "50px" }}
/>
@ -115,29 +137,31 @@ export class Bus extends React.Component<BusProps, BusState> {
marginInline: "15px",
}}
// 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}
onChange={(ev) =>
this.onChange(
ev as unknown as React.ChangeEvent<HTMLInputElement>
)
}
onKeyDown={(ev) => this.onKeyDown(ev)}
onChange={(e) => this.onGainChange(e)}
onClick={(e) => this.onSliderClick(e)}
/>
<ButtonGroup>
<Button
onClick={() => this.onMuteToggle()}
onClick={(e) => this.onMuteToggle(e)}
variant={this.state.muted ? "contained" : "outlined"}
color={"error"}
>
Mute
</Button>
<Button
onClick={() => this.onSelectToggle()}
onClick={(e) => this.onSelectToggle(e)}
sx={{}}
variant={this.state.selected ? "contained" : "outlined"}
color={"warning"}

View File

@ -0,0 +1,61 @@
import { Stack, Theme, Typography, useTheme, withTheme } from "@mui/material";
import React from "react";
import { Bus } from "./Bus";
import { range } from "@/utils/Range";
export interface BusesListProps {
physical: number;
virtual: number;
width: number;
}
export interface BusesListState {
}
export class BusesList extends React.Component<BusesListProps, BusesListState> {
constructor(props: BusesListProps) {
super(props);
}
mapCallback(
...args: Parameters<
Parameters<Parameters<typeof Array<number>>["map"]>["0"]
>
) {
const [busId] = args;
const isPhysical = busId < this.props.physical;
return (
<React.Fragment
key={`${isPhysical ? "A" : "B"}${
isPhysical ? busId + 1 : busId - this.props.physical + 1
}`}
>
<Typography variant="caption">{`${isPhysical ? "A" : "B"}${
isPhysical ? busId + 1 : busId - this.props.physical + 1
}`}</Typography>
<Bus onChange={console.log} />
</React.Fragment>
);
}
render(): React.ReactNode {
if (this.props.width > 600) {
return (
<Stack direction={"row"} spacing={this.props.width > 1200 ? 4 : 2}>
<Stack sx={{ width: "50%" }}>
{range(this.props.physical).map(this.mapCallback.bind(this))}
</Stack>
<Stack sx={{ width: "50%" }}>
{range(this.props.virtual, this.props.physical).map(
this.mapCallback.bind(this)
)}
</Stack>
</Stack>
);
}
return (
<Stack>
{range(this.props.physical + this.props.virtual).map(
this.mapCallback.bind(this)
)}
</Stack>
);
}
}

View File

@ -10,6 +10,7 @@ import {
} from "@mui/material";
import React from "react";
import { StripBusOutput, StripBusOutputEvent } from "./StripBusOutput";
import { EventCounter } from "@/utils/EventCounter";
export interface StripProps {
physicalBuses: number;
@ -40,32 +41,42 @@ export interface StripMuted {
}
export class Strip extends React.Component<StripProps, StripState> {
private defaultValues: StripState;
private eventCounter = new EventCounter<"onSliderResetDefaults">();
constructor(props: StripProps) {
super(props);
const { values: initialValues } = props;
const defaultValues: StripState = {
this.defaultValues = {
gain: 0,
outputBuses: [...Array(props.physicalBuses + props.virtualBuses)].map(
(_v, k) => false
),
muted: false,
};
const getValue = (name: keyof typeof defaultValues) => {
if (initialValues === undefined) return defaultValues[name];
const getValue = (name: keyof typeof this.defaultValues) => {
if (initialValues === undefined) return this.defaultValues[name];
return initialValues[name] !== undefined
? defaultValues[name]
? this.defaultValues[name]
: initialValues[name];
};
this.state = Object.fromEntries(
Object.entries(defaultValues).map(([name]) => [
Object.entries(this.defaultValues).map(([name]) => [
name,
getValue(name as keyof typeof defaultValues) as any,
getValue(name as keyof typeof this.defaultValues) as any,
])
) as StripState;
this.eventCounter.on({
callback: this.onResetDefaults.bind(this),
count: 2,
name: "onSliderResetDefaults",
});
}
onResetDefaults(e: React.MouseEvent) {
e.stopPropagation();
this.setState({ gain: 0 });
onResetDefaults() {
this.setState({ gain: this.defaultValues.gain });
}
onSliderClick(event: React.MouseEvent) {
this.stopPropagation(event);
this.eventCounter.emit("onSliderResetDefaults");
}
onKeyDown(event: React.KeyboardEvent) {
console.log(event);
@ -74,7 +85,8 @@ export class Strip extends React.Component<StripProps, StripState> {
event.preventDefault();
}
}
onChange(event: React.ChangeEvent<HTMLInputElement>) {
onGainChange(event: Event) {
this.stopPropagation(event);
const multiplier = (event as unknown as React.KeyboardEvent).shiftKey
? 2
: 1;
@ -106,7 +118,8 @@ export class Strip extends React.Component<StripProps, StripState> {
onStripWheel(event: React.WheelEvent) {
console.log(event);
}
onMuteToggle() {
onMuteToggle(event: React.MouseEvent) {
this.stopPropagation(event);
this.setState(
(state) => ({ muted: !state.muted }),
() => {
@ -114,6 +127,10 @@ export class Strip extends React.Component<StripProps, StripState> {
}
);
}
stopPropagation(event: Event | React.MouseEvent) {
if (event instanceof Event) event.stopImmediatePropagation();
else event.nativeEvent.stopImmediatePropagation();
}
render() {
return (
<>
@ -123,17 +140,25 @@ export class Strip extends React.Component<StripProps, StripState> {
inputProps={{
"aria-labelledby": "input-slider",
itemType: "number",
style: { padding: 0 },
}}
value={this.state.gain}
onWheel={(ev) => this.onStripWheel(ev)}
onWheel={(e) => this.onStripWheel(e)}
onClick={(e) => this.stopPropagation(e)}
size="small"
sx={{ marginInline: "5px" }}
/>
<Slider
sx={{
margin: "5px",
marginBlock: "15px",
marginBlockStart: "15px",
}}
color={
(this.state.gain > 0 && this.state.gain < 12 && "warning") ||
(this.state.gain >= 12 && "error") ||
(this.state.gain < 0 && "info") ||
"primary"
}
orientation="vertical"
defaultValue={0.0}
step={0.1}
@ -141,26 +166,12 @@ export class Strip extends React.Component<StripProps, StripState> {
max={12}
value={this.state.gain}
aria-label="Temperature"
onChange={(ev) =>
this.onChange(
ev as unknown as React.ChangeEvent<HTMLInputElement>
)
}
onKeyDown={(ev) => this.onKeyDown(ev)}
onChange={(e) => this.onGainChange(e)}
onClick={(e) => this.onSliderClick(e)}
/>
<Button
onClick={() => this.onMuteToggle()}
variant={this.state.muted ? "contained" : "outlined"}
color={"error"}
style={{ paddingInline: "0px", maxWidth: "5px" }}
size="large"
>
Mute
</Button>
</Stack>
<Stack>
<ButtonGroup orientation="vertical">
<ButtonGroup orientation="vertical" sx={{paddingInlineStart:"5px"}}>
{[
...Array(this.props.physicalBuses + this.props.virtualBuses),
].map((_v, i) => (
@ -176,10 +187,18 @@ export class Strip extends React.Component<StripProps, StripState> {
: i - this.props.physicalBuses
}
isVirtual={i < this.props.physicalBuses ? false : true}
onChange={(ev) => this.onBusChange(ev)}
onChange={(e) => this.onBusChange(e)}
default={{ enabled: this.state.outputBuses[i] }}
/>
))}
<Button
onClick={(e) => this.onMuteToggle(e)}
variant={this.state.muted ? "contained" : "outlined"}
color={"error"}
size="small"
>
Mute
</Button>
</ButtonGroup>
</Stack>
</Stack>

View File

@ -30,6 +30,7 @@ export class StripBusOutput extends React.Component<StripProps, StripState> {
};
}
onClick(event: React.MouseEvent) {
event.nativeEvent.stopImmediatePropagation()
this.setState(
(state) => ({ enabled: !state.enabled }),
() => {
@ -47,8 +48,8 @@ export class StripBusOutput extends React.Component<StripProps, StripState> {
<>
<Button
size="small"
sx={{ width: "5px", height: "35px" }}
onClick={(ev) => this.onClick(ev)}
sx={{ height: "35px" }}
onClick={(e) => this.onClick(e)}
variant={this.state.enabled ? "contained" : "outlined"}
>
{this.props.isVirtual ? "B" : "A"}

View File

@ -1,33 +0,0 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
:root {
--foreground-rgb: 0, 0, 0;
--background-start-rgb: 214, 219, 220;
--background-end-rgb: 255, 255, 255;
}
@media (prefers-color-scheme: dark) {
:root {
--foreground-rgb: 255, 255, 255;
--background-start-rgb: 0, 0, 0;
--background-end-rgb: 0, 0, 0;
}
}
body {
color: rgb(var(--foreground-rgb));
background: linear-gradient(
to bottom,
transparent,
rgb(var(--background-end-rgb))
)
rgb(var(--background-start-rgb));
}
@layer utilities {
.text-balance {
text-wrap: balance;
}
}

View File

@ -16,6 +16,9 @@ import {
} from "@mui/material";
import { Bus } from "./component/Bus";
import Grid from "@mui/material/Unstable_Grid2";
import { EventCounter } from "@/utils/EventCounter";
import { BusesList } from "./component/BusesList";
import { isItMobile } from "@/utils/DetectMobile";
function random(min: number, max: number, floor: boolean = true) {
const value = Math.random() * (max - min + 1) + min;
@ -23,7 +26,7 @@ function random(min: number, max: number, floor: boolean = true) {
}
export default function Home() {
const theme = useMemo(() => createTheme({ palette: { mode: "dark" } }), []);
const theme = useMemo(() => createTheme({ palette: { mode: "dark",background:{default: "black"} } }), []);
function onStripEvent(event: StripEvent) {
console.log(event);
}
@ -34,61 +37,64 @@ export default function Home() {
const [snackBarVisible, setSnackBarVisibility] = useState(false);
const breakPoints = useMemo(() => theme.breakpoints.values, []);
const [width, setWidth] = useState(
typeof document !== "undefined" ? window.innerWidth : 0
1000
);
const eventCounter = useMemo(() => {
const eventCounter = new EventCounter<"emptySpaceClick">();
eventCounter.on({
callback: () => {
if (document.fullscreenElement === null)
document.documentElement.requestFullscreen();
else document.exitFullscreen();
},
count: 3,
name: "emptySpaceClick",
});
return eventCounter;
}, []);
const isItMobileDevice = useMemo(() => isItMobile(navigator.userAgent), []);
useEffect(() => {
setSnackBarVisibility(!isSnackBarHidden);
setWidth(window.innerWidth);
}, []);
useEffect(() => {
const values = {} as Record<string, [number, "bigger" | "smaller" | "equal"]>;
type valuesKey = keyof typeof breakPoints;
for (const key in breakPoints) {
if (Object.prototype.hasOwnProperty.call(breakPoints, key)) {
const breakPointWidth = breakPoints[key as keyof typeof breakPoints];
values[key as valuesKey] =
[breakPointWidth, breakPointWidth === width
? "equal"
: breakPointWidth > width
? "bigger"
: "smaller"];
}
}
console.clear()
console.log(
width,
`Closest: ${
Object.entries(breakPoints).reduce((acc, i) => {
const equation = Math.abs(i[1] - width) <= Math.abs(acc[1] - width) ? i : acc;
return equation;
})[0]
}`,
);
console.table(values);
}, [width]);
// useEffect(() => {
// const values = {} as Record<
// string,
// [number, "bigger" | "smaller" | "equal"]
// >;
// type valuesKey = keyof typeof breakPoints;
// for (const key in breakPoints) {
// if (Object.prototype.hasOwnProperty.call(breakPoints, key)) {
// const breakPointWidth = breakPoints[key as keyof typeof breakPoints];
// values[key as valuesKey] = [
// breakPointWidth,
// breakPointWidth === width
// ? "equal"
// : breakPointWidth > width
// ? "bigger"
// : "smaller",
// ];
// }
// }
// console.clear();
// console.log(
// width,
// `Closest: ${
// Object.entries(breakPoints).reduce((acc, i) => {
// const equation =
// Math.abs(i[1] - width) <= Math.abs(acc[1] - width) ? i : acc;
// return equation;
// })[0]
// }`
// );
// console.table(values);
// }, [width]);
const strips = { virtual: 3, physical: 5 };
const buses = { virtual: 3, physical: 5 };
const amountOfStrips = strips.physical + strips.virtual;
const amountOfBuses = buses.physical + buses.virtual;
let clicks = 0;
let timeout: NodeJS.Timeout | undefined;
if (typeof document !== "undefined") {
document.onclick = (e) => {
clicks++;
if (timeout !== undefined) {
clearTimeout(timeout);
timeout = undefined;
}
timeout = setTimeout(() => (clicks = 0), 250);
console.log(e);
if (clicks < 2) return;
clearTimeout(timeout);
clicks = 0;
// e.stopPropagation();
// if (document.fullscreenElement !== null) document.exitFullscreen();
// else document.body.requestFullscreen({ navigationUI: "hide" });
};
document.onclick = () => eventCounter.emit("emptySpaceClick");
window.onresize = () => {
setWidth(window.innerWidth);
};
@ -109,17 +115,21 @@ export default function Home() {
);
return (
<ThemeProvider theme={theme}>
<Container maxWidth={"xl"} sx={{ height: "100%", paddingInline: "5px" }}>
<Container
id="container"
maxWidth={"xl"}
style={{ height: "100%", paddingInline: "10px" }}
>
<div>
<CssBaseline />
<Typography variant="h5">Inputs</Typography>
<Grid
container
spacing={width ? 0 : 1}
// spacing={width ? 0 : 1}
display={"flex"}
alignItems={"center"}
justifyContent={"space-evenly"}
columns={{ xs: 8, sm: 12, md: 5, lg: 12 }}
columns={{ xs: 8, sm: 12, md: 16, lg: 12 }}
>
{[...Array(amountOfStrips)].map((_v, stripId) => (
<React.Fragment
@ -129,22 +139,9 @@ export default function Home() {
: stripId + 1 - amountOfStrips
}`}
>
{stripId === amountOfStrips && width > breakPoints.lg && (
<Grid>
<Divider
sx={{ marginBlock: "15px", borderWidth: "1px" }}
variant="fullWidth"
orientation="vertical"
>
Virtual Inputs
</Divider>
</Grid>
)}
<Grid
lg={stripId + 1 === strips.physical ? 2 : 0}
xs={1}
sm={1}
md={0}
lgOffset={stripId === strips.physical ? 2 : 0}
mdOffset={stripId === strips.physical ? 0.5 : 0}
sx={{ minWidth: "fit-content" }}
>
<Typography variant="overline">{`Strip #${
@ -159,29 +156,8 @@ export default function Home() {
</React.Fragment>
))}
</Grid>
<Typography variant="h5">Outputs</Typography>
<Grid container spacing={1} direction={"column"}>
{[...Array(amountOfBuses)].map((_v, busId) => (
<Grid
key={`${busId < buses.physical ? "a" : "b"}${
busId < buses.physical
? busId + 1
: busId + 1 - buses.physical
}`}
xs={12}
>
<Typography variant="overline">{`Bus ${
busId < buses.physical ? "a" : "b"
}${
busId < buses.physical
? busId + 1
: busId + 1 - buses.physical
}`}</Typography>
<Bus onChange={console.log} />
</Grid>
))}
</Grid>
<BusesList width={width} physical={buses.physical} virtual={buses.virtual} />
</div>
</Container>
<Snackbar
@ -189,7 +165,23 @@ export default function Home() {
TransitionComponent={Slide}
transitionDuration={250}
open={snackBarVisible}
message="Triple tap on empty space for toggle fullscreen"
message={
<>
<Typography fontWeight={600}>Tips:</Typography>
<Typography component={"span"}>
<Typography variant={"button"} fontWeight={500}>
Double {isItMobileDevice? "tap": "click"}
</Typography>{" "}
on any gain slider to reset it's value to 0 dB<br/>
</Typography>
<Typography component={"span"}>
<Typography variant={"button"} fontWeight={500}>
Triple {isItMobileDevice? "tap": "click"}
</Typography>{" "}
on empty space for toggle fullscreen
</Typography>
</>
}
action={snackBarActionButton}
/>
</ThemeProvider>

10
src/utils/DetectMobile.ts Normal file
View File

@ -0,0 +1,10 @@
export function isItMobile(userAgent: string) {
return (
/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino/i.test(
userAgent
) ||
/1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(
userAgent.substring(0, 4)
)
);
}

View File

@ -1,29 +1,55 @@
interface EventListener {
name: keyof EventCounter["events"];
callback: Function | Promise<any>;
callback: Function;
count: number;
}
type EventName = string;
type EventInfo = number[];
export class EventCounter {
events: Record<EventName, EventInfo> = {};
type EventInfo = number;
export class EventCounter<T extends string> {
events: Record<T, EventInfo> = {} as Record<T, EventInfo>;
timer: Record<EventName, NodeJS.Timeout | undefined> = {};
listeners: Record<EventName, EventListener[]> = {};
private checkListeners(eventName: EventListener["name"]) {
this.events[eventName];
constructor(public timeout: number = 200) {}
private getMaxClicksLimit(eventName: T) {
if(this.listeners[eventName] === undefined) return 0;
return Math.max(...this.listeners[eventName].map((e) => e.count));
}
private countEmits(eventName: EventListener["name"], periodOfTime: number) {}
emit(eventName: EventListener["name"]) {
this.events[eventName].length >= 100 && this.events[eventName].shift();
}
on(args: EventListener) {
private emitListeners(eventName: T) {
const counter = this.events[eventName];
this.events[eventName] = 0;
const listeners = this.listeners[eventName].filter(
(e) => e.count === counter
);
for (const listener of listeners) {
listener.callback();
}
}
emit(eventName: T) {
if (eventName in this.events === undefined) this.events[eventName] = 0;
if (this.timer[eventName] !== undefined) {
clearTimeout(this.timer[eventName]);
this.timer[eventName] = undefined;
}
const timeoutCallback = () => {
this.emitListeners(eventName);
this.timer[eventName] = undefined;
};
this.events[eventName]++;
console.log(this.events);
if (this.events[eventName] >= this.getMaxClicksLimit(eventName))
return timeoutCallback();
this.timer[eventName] = setTimeout(timeoutCallback, this.timeout);
}
on(args: EventListener & { name: T }) {
const { name: eventName, callback, count } = args;
if (eventName in this.events === undefined) this.events[eventName] = [];
if (!(eventName in this.events)) this.events[eventName] = 0;
if (!Object.prototype.hasOwnProperty.call(this.listeners, eventName))
this.listeners[eventName] = [];
this.listeners[eventName].push({ callback, count, name: eventName });
this.listeners[eventName].push({ callback, count });
}
}

3
src/utils/Range.ts Normal file
View File

@ -0,0 +1,3 @@
export function range(size: number, from: number = 0) {
return [...Array(size).keys()].map((i) => i + from);
}

View File

@ -9,6 +9,7 @@
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"target": "ES2023",
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,