Formatted code

This commit is contained in:
Liam 2022-10-14 20:00:47 +01:00
parent 5b1c408fb9
commit 7c713a99ab
22 changed files with 759 additions and 586 deletions

@ -1,49 +1,55 @@
// 1. import `NextUIProvider` component // 1. import `NextUIProvider` component
import { NextUIProvider } from '@nextui-org/react'; import { NextUIProvider } from "@nextui-org/react";
import { NextSeo } from 'next-seo'; import { NextSeo } from "next-seo";
import Head from 'next/head' import Head from "next/head";
import Config from '../config.json'; import Config from "../config.json";
import '../styles/globals.css'; import "../styles/globals.css";
function MyApp({ Component, pageProps }) { function MyApp({ Component, pageProps }) {
return ( return (
// 2. Use at the root of your app // 2. Use at the root of your app
<NextUIProvider> <NextUIProvider>
<NextSeo <NextSeo
title={Config.name} title={Config.name}
description={Config.description} description={Config.description}
openGraph={{ openGraph={{
url: Config.url, url: Config.url,
title: Config.name, title: Config.name,
description: Config.description, description: Config.description,
site_name: Config.name, site_name: Config.name,
images: [ images: [
{ {
url: "https://cdn.fascinated.cc/YrATaLjUOP.png?raw=true", url: "https://cdn.fascinated.cc/YrATaLjUOP.png?raw=true",
alt: "Site Example" alt: "Site Example",
} },
] ],
}} }}
twitter={{ twitter={{
cardType: "summary_large_image", cardType: "summary_large_image",
site: "@BeatSaber Overlay", site: "@BeatSaber Overlay",
}} }}
/> />
<Head> <Head>
<meta name="theme-color" content= {Config.color} /> <meta name="theme-color" content={Config.color} />
<meta property="og:keywords" content="BeatSaber,Overlay,OBS,Twitch,YouTube,BeatSaber Overlay,Github," /> <meta
property="og:keywords"
content="BeatSaber,Overlay,OBS,Twitch,YouTube,BeatSaber Overlay,Github,"
/>
<noscript> <noscript>
<img src="https://analytics.fascinated.cc/ingress/4bc413fa-a126-4860-9a6a-22d10d5cf2fb/pixel.gif" /> <img src="https://analytics.fascinated.cc/ingress/4bc413fa-a126-4860-9a6a-22d10d5cf2fb/pixel.gif" />
</noscript> </noscript>
<script defer src="https://analytics.fascinated.cc/ingress/4bc413fa-a126-4860-9a6a-22d10d5cf2fb/script.js"></script> <script
</Head> defer
<Component {...pageProps} /> src="https://analytics.fascinated.cc/ingress/4bc413fa-a126-4860-9a6a-22d10d5cf2fb/script.js"
</NextUIProvider> ></script>
); </Head>
<Component {...pageProps} />
</NextUIProvider>
);
} }
export default MyApp; export default MyApp;

@ -1,37 +1,37 @@
import React from 'react'; import { CssBaseline } from "@nextui-org/react";
import Document, { Html, Head, Main, NextScript } from 'next/document'; import Document, { Head, Html, Main, NextScript } from "next/document";
import { CssBaseline } from '@nextui-org/react'; import React from "react";
class MyDocument extends Document { class MyDocument extends Document {
static async getInitialProps(ctx) { static async getInitialProps(ctx) {
const initialProps = await Document.getInitialProps(ctx); const initialProps = await Document.getInitialProps(ctx);
return { return {
...initialProps, ...initialProps,
styles: React.Children.toArray([initialProps.styles]) styles: React.Children.toArray([initialProps.styles]),
}; };
} }
render() { render() {
return ( return (
<Html lang="en"> <Html lang="en">
<Head> <Head>
{CssBaseline.flush()} {CssBaseline.flush()}
<link <link
href="https://fonts.googleapis.com/css2?family=Roboto:wght@300&display=swap" href="https://fonts.googleapis.com/css2?family=Roboto:wght@300&display=swap"
rel="stylesheet" rel="stylesheet"
/> />
<link <link
href="https://fonts.googleapis.com/css2?family=Teko:wght@300&display=swap" href="https://fonts.googleapis.com/css2?family=Teko:wght@300&display=swap"
rel="stylesheet" rel="stylesheet"
/> />
</Head> </Head>
<body> <body>
<Main /> <Main />
<NextScript /> <NextScript />
</body> </body>
</Html> </Html>
); );
} }
} }
export default MyDocument; export default MyDocument;

@ -1,31 +1,31 @@
import fs from 'fs'; import fs from "fs";
import path from 'path'; import fetch from "node-fetch";
import sharp from 'sharp'; import path from "path";
import fetch from 'node-fetch'; import sharp from "sharp";
const cacheDir = process.cwd() + path.sep + "cache"; const cacheDir = process.cwd() + path.sep + "cache";
if (!fs.existsSync(cacheDir)) { if (!fs.existsSync(cacheDir)) {
fs.mkdirSync(cacheDir); fs.mkdirSync(cacheDir);
console.log("Created cache directory"); console.log("Created cache directory");
} }
export default async function handler(req, res) { export default async function handler(req, res) {
const mapHash = req.query.hash.replace("custom_level_", "").toLowerCase(); const mapHash = req.query.hash.replace("custom_level_", "").toLowerCase();
const ext = req.query.ext; const ext = req.query.ext;
const imagePath = cacheDir + path.sep + mapHash + "." + ext; const imagePath = cacheDir + path.sep + mapHash + "." + ext;
const exists = fs.existsSync(imagePath); const exists = fs.existsSync(imagePath);
if (!exists) { if (!exists) {
const data = await fetch(`https://eu.cdn.beatsaver.com/${mapHash}.${ext}`); const data = await fetch(`https://eu.cdn.beatsaver.com/${mapHash}.${ext}`);
let buffer = await data.buffer(); let buffer = await data.buffer();
buffer = await sharp(buffer).resize(150, 150).toBuffer(); buffer = await sharp(buffer).resize(150, 150).toBuffer();
fs.writeFileSync(imagePath, buffer); fs.writeFileSync(imagePath, buffer);
res.setHeader('Content-Type', 'image/' + ext); res.setHeader("Content-Type", "image/" + ext);
res.send(buffer); res.send(buffer);
console.log("Song Art Cache - Added song \"" + mapHash + "\""); console.log('Song Art Cache - Added song "' + mapHash + '"');
return; return;
} }
const buffer = fs.readFileSync(imagePath); const buffer = fs.readFileSync(imagePath);
res.setHeader('Content-Type', 'image/jpg' + ext); res.setHeader("Content-Type", "image/jpg" + ext);
res.send(buffer); res.send(buffer);
} }

@ -1,16 +1,23 @@
import Utils from '../../../utils/utils'; import Utils from "../../../utils/utils";
export default async function handler(req, res) { export default async function handler(req, res) {
const mapHash = req.query.hash; const mapHash = req.query.hash;
const mapData = await Utils.getMapData(mapHash.replace("custom_level_", "")); const mapData = await Utils.getMapData(mapHash.replace("custom_level_", ""));
if (mapData === undefined) { // Check if a map hash was provided if (mapData === undefined) {
return res.json({ error: true, message: "Unknown map" }); // Check if a map hash was provided
} return res.json({ error: true, message: "Unknown map" });
const data = { // The maps data from the provided map hash }
bsr: mapData.id, const data = {
songArt: "http://" + req.headers.host + "/api/beatsaver/art/" + mapHash + "?ext=" + mapData.versions[0].coverURL // The maps data from the provided map hash
.split("/")[3].split(".")[1] bsr: mapData.id,
}; songArt:
res.json({ error: false, data: data }); "http://" +
req.headers.host +
"/api/beatsaver/art/" +
mapHash +
"?ext=" +
mapData.versions[0].coverURL.split("/")[3].split(".")[1],
};
res.json({ error: false, data: data });
} }

@ -1,9 +1,10 @@
export default async function handler(req, res) { export default async function handler(req, res) {
res.json({ res.json({
"avatar": "https://avatars.akamai.steamstatic.com/4322d8d20cb6dbdd1d891b4efa9952a9679c9a76_full.jpg", avatar:
"country": "GB", "https://avatars.akamai.steamstatic.com/4322d8d20cb6dbdd1d891b4efa9952a9679c9a76_full.jpg",
"pp": 0, country: "GB",
"rank": 0, pp: 0,
"countryRank": 0, rank: 0,
}); countryRank: 0,
});
} }

@ -1,14 +1,24 @@
import { Button, Card, Container, Grid, Input, Link, Modal, Spacer, Switch, Text, textTransforms } from '@nextui-org/react'; import {
import { Component } from 'react'; Button,
import NavBar from '../src/components/Navbar'; Card,
Container,
Grid,
Input,
Link,
Modal,
Spacer,
Switch,
Text,
} from "@nextui-org/react";
import { Component } from "react";
import NavBar from "../src/components/Navbar";
import { ToastContainer, toast } from 'react-toastify'; import { toast, ToastContainer } from "react-toastify";
import 'react-toastify/dist/ReactToastify.css'; import "react-toastify/dist/ReactToastify.css";
import styles from '../styles/main.module.css'; import styles from "../styles/main.module.css";
export default class Home extends Component { export default class Home extends Component {
constructor(props) { constructor(props) {
super(props); super(props);
@ -26,7 +36,7 @@ export default class Home extends Component {
showScoreInfo: false, showScoreInfo: false,
showSongInfo: false, showSongInfo: false,
}, },
} };
} }
async componentDidMount() { async componentDidMount() {
@ -34,29 +44,35 @@ export default class Home extends Component {
const params = Object.fromEntries(urlSearchParams.entries()); const params = Object.fromEntries(urlSearchParams.entries());
if (params.id) { if (params.id) {
document.location.href = "/overlay/"+ window.location.search document.location.href = "/overlay/" + window.location.search;
return; return;
} }
if (localStorage.getItem('values') == undefined) { if (localStorage.getItem("values") == undefined) {
localStorage.setItem('values', JSON.stringify({ localStorage.setItem(
steamId: this.state.steamId, "values",
values: this.state.values JSON.stringify({
})); steamId: this.state.steamId,
values: this.state.values,
})
);
} else { } else {
const json = JSON.parse(localStorage.getItem('values')) const json = JSON.parse(localStorage.getItem("values"));
this.setState({ steamId: json.steamId, values: json.values }); this.setState({ steamId: json.steamId, values: json.values });
} }
this.setState({ loading: false }); this.setState({ loading: false });
} }
loadPreview() { loadPreview() {
this.setState({ isPreviewVisible: true, previewUrl: this.generateUrl(true) }); this.setState({
isPreviewVisible: true,
previewUrl: this.generateUrl(true),
});
} }
generateUrl(withTc = false) { generateUrl(withTc = false) {
let values = ""; let values = "";
Object.entries(this.state.values).forEach(value => { Object.entries(this.state.values).forEach((value) => {
if (value[1] === undefined) { if (value[1] === undefined) {
return; return;
} }
@ -67,7 +83,13 @@ export default class Home extends Component {
values += `&${value[0]}=${value[1]}`; values += `&${value[0]}=${value[1]}`;
}); });
return window.location.origin + "/overlay?id=" + this.state.steamId + values + (withTc ? "&textColor=black" : ""); return (
window.location.origin +
"/overlay?id=" +
this.state.steamId +
values +
(withTc ? "&textColor=black" : "")
);
} }
updateValue(key, value) { updateValue(key, value) {
@ -79,122 +101,185 @@ export default class Home extends Component {
updateStorage() { updateStorage() {
setTimeout(() => { setTimeout(() => {
localStorage.setItem('values', JSON.stringify({ localStorage.setItem(
steamId: this.state.steamId, "values",
values: this.state.values JSON.stringify({
})); steamId: this.state.steamId,
values: this.state.values,
})
);
}, 5); }, 5);
} }
render() { render() {
return this.state.loading ? <h1>Loading...</h1> : return this.state.loading ? (
<div className={styles.main}> <h1>Loading...</h1>
<NavBar></NavBar> ) : (
<div className={styles.main}>
<NavBar></NavBar>
<Container css={{ <Container
marginTop: '$8' css={{
}}> marginTop: "$8",
}}
>
{/* Preview */}
{this.state.isPreviewVisible ? (
<Modal
closeButton
open={this.state.isPreviewVisible}
width={"100%"}
blur
onClose={() => this.setState({ isPreviewVisible: false })}
>
<Modal.Header>
<Text size={18}>Overlay Preview</Text>
</Modal.Header>
<Modal.Body>
<iframe height={600} src={this.state.previewUrl}></iframe>
</Modal.Body>
</Modal>
) : (
<></>
)}
{/* Preview */} <Grid.Container gap={2} justify="center">
{ <Grid
this.state.isPreviewVisible ? <Modal xs={12}
closeButton css={{
open={this.state.isPreviewVisible} color: "black",
width={"100%"} }}
blur justify="center"
onClose={() => this.setState({ isPreviewVisible: false })} >
> <div
<Modal.Header> style={{
<Text size={18}> textAlign: "center",
Overlay Preview }}
</Text> >
</Modal.Header> <Text h1>BeatSaber Overlay</Text>
<Modal.Body> <Text h4>Welcome to the Setup panel</Text>
<iframe height={600} src={this.state.previewUrl}></iframe> </div>
</Modal.Body> </Grid>
</Modal> : <></>
}
<Grid.Container gap={2} justify='center'> <Grid xs={12}>
<Grid xs={12} css={{ <Card>
color: 'black' <Card.Body>
}} justify='center'> <Spacer y={1} />
<div style={{
textAlign: 'center',
}}>
<Text h1>BeatSaber Overlay</Text>
<Text h4>Welcome to the Setup panel</Text>
</div>
</Grid>
<Grid xs={12}> <Input
<Card> underlined
<Card.Body> labelPlaceholder="Ip Address (Only set if you stream on multiple devices)"
<Spacer y={1} /> initialValue="localhost"
value={this.state.values.socketAddr}
<Input onChange={(event) =>
underlined this.updateValue("socketAddr", event.target.value)
labelPlaceholder="Ip Address (Only set if you stream on multiple devices)"
initialValue="localhost"
value={this.state.values.socketAddr}
onChange={event => this.updateValue("socketAddr", event.target.value)} checked={true}
/>
<Spacer y={2} />
<Input
underlined
labelPlaceholder="Steam Id"
initialValue=""
value={this.state.steamId}
onChange={event => {
this.setState({ steamId: event.target.value });
this.updateStorage();
}}
/>
<Spacer y={1} />
<Text>Do you want to use BeatLeader rather than ScoreSaber?</Text>
<Switch onChange={event => this.updateValue("useBeatLeader", event.target.checked)} checked={this.state.values.useBeatLeader} size="md" />
<Text>Do you want to show Player Stats (Current PP, global pos, etc)</Text>
<Switch onChange={event => this.updateValue("showPlayerStats", event.target.checked)} checked={this.state.values.showPlayerStats} size="md" />
<Text>Do you want to show Score Info (Current swing values, total score, etc)</Text>
<Switch onChange={event => this.updateValue("showScoreInfo", event.target.checked)} checked={this.state.values.showScoreInfo} size="md" />
<Text>Do you want to show Song Info (Song name, bsr, song art, etc)</Text>
<Switch onChange={event => this.updateValue("showSongInfo", event.target.checked)} checked={this.state.values.showSongInfo} size="md" />
<Spacer y={1} />
<Button.Group>
<Button flat auto onClick={() => {
if (!this.state.steamId) {
toast.error("Please provide a Steam ID");
return;
} }
window.open(this.generateUrl(), "_blank"); checked={true}
}}>Open Overlay</Button> />
<Button flat auto onClick={() => { <Spacer y={2} />
if (!this.state.steamId) { <Input
toast.error("Please provide a Steam ID"); underlined
return; labelPlaceholder="Steam Id"
initialValue=""
value={this.state.steamId}
onChange={(event) => {
this.setState({ steamId: event.target.value });
this.updateStorage();
}}
/>
<Spacer y={1} />
<Text>
Do you want to use BeatLeader rather than ScoreSaber?
</Text>
<Switch
onChange={(event) =>
this.updateValue("useBeatLeader", event.target.checked)
} }
this.loadPreview(); checked={this.state.values.useBeatLeader}
}}>Preview</Button> size="md"
</Button.Group> />
<Text>
Do you want to show Player Stats (Current PP, global pos,
etc)
</Text>
<Switch
onChange={(event) =>
this.updateValue("showPlayerStats", event.target.checked)
}
checked={this.state.values.showPlayerStats}
size="md"
/>
<Text>
Do you want to show Score Info (Current swing values, total
score, etc)
</Text>
<Switch
onChange={(event) =>
this.updateValue("showScoreInfo", event.target.checked)
}
checked={this.state.values.showScoreInfo}
size="md"
/>
<Text>
Do you want to show Song Info (Song name, bsr, song art,
etc)
</Text>
<Switch
onChange={(event) =>
this.updateValue("showSongInfo", event.target.checked)
}
checked={this.state.values.showSongInfo}
size="md"
/>
<Spacer y={1} />
{ <Button.Group>
this.state.overlayUrl !== undefined ? <Button
flat
auto
onClick={() => {
if (!this.state.steamId) {
toast.error("Please provide a Steam ID");
return;
}
window.open(this.generateUrl(), "_blank");
}}
>
Open Overlay
</Button>
<Button
flat
auto
onClick={() => {
if (!this.state.steamId) {
toast.error("Please provide a Steam ID");
return;
}
this.loadPreview();
}}
>
Preview
</Button>
</Button.Group>
{this.state.overlayUrl !== undefined ? (
<> <>
<Text b> <Text b>Url</Text>
Url <Link href={this.state.overlayUrl}>
</Text> {this.state.overlayUrl}
<Link href={this.state.overlayUrl}>{this.state.overlayUrl}</Link> </Link>
</> </>
: <></> ) : (
} <></>
</Card.Body> )}
</Card> </Card.Body>
</Grid> </Card>
</Grid.Container> </Grid>
</Container> </Grid.Container>
</Container>
<ToastContainer /> <ToastContainer />
</div> </div>
} );
}
} }

@ -1,14 +1,13 @@
import { Link } from '@nextui-org/react'; import { Link } from "@nextui-org/react";
import {Component} from 'react' import { Component } from "react";
import PlayerStats from '../src/components/PlayerStats'; import PlayerStats from "../src/components/PlayerStats";
import ScoreStats from '../src/components/ScoreStats'; import ScoreStats from "../src/components/ScoreStats";
import SongInfo from "../src/components/SongInfo"; import SongInfo from "../src/components/SongInfo";
import Utils from '../src/utils/utils'; import Utils from "../src/utils/utils";
import styles from '../styles/overlay.module.css'; import styles from "../styles/overlay.module.css";
export default class Overlay extends Component { export default class Overlay extends Component {
#_beatSaverURL = ""; #_beatSaverURL = "";
constructor(props) { constructor(props) {
@ -36,16 +35,16 @@ export default class Overlay extends Component {
percentage: "100.00%", percentage: "100.00%",
failed: false, failed: false,
leftHand: { leftHand: {
averageCut: [15.00], averageCut: [15.0],
averagePreSwing: [70.00], averagePreSwing: [70.0],
averagePostSwing: [30.00], averagePostSwing: [30.0],
}, },
rightHand: { rightHand: {
averageCut: [15.00], averageCut: [15.0],
averagePreSwing: [70.00], averagePreSwing: [70.0],
averagePostSwing: [30.00], averagePostSwing: [30.0],
} },
} };
this.setupTimer(); this.setupTimer();
} }
@ -60,10 +59,10 @@ export default class Overlay extends Component {
setupTimer() { setupTimer() {
setInterval(() => { setInterval(() => {
if (this.isCurrentSongTimeProvided) { if (this.isCurrentSongTimeProvided) {
return return;
} }
if (!this.state.paused && this.state.beatSaverData !== undefined) { if (!this.state.paused && this.state.beatSaverData !== undefined) {
this.setState({ currentSongTime: this.state.currentSongTime + 1 }) this.setState({ currentSongTime: this.state.currentSongTime + 1 });
} }
}, 1000); }, 1000);
} }
@ -75,10 +74,10 @@ export default class Overlay extends Component {
*/ */
handleCurrentSongTime(data) { handleCurrentSongTime(data) {
try { try {
const time = data.status.performance.currentSongTime const time = data.status.performance.currentSongTime;
if (time !== undefined && time != null) { if (time !== undefined && time != null) {
this.isCurrentSongTimeProvided = true this.isCurrentSongTimeProvided = true;
this.setState({ currentSongTime: time }) this.setState({ currentSongTime: time });
} }
} catch (e) { } catch (e) {
// do nothing // do nothing
@ -86,24 +85,26 @@ export default class Overlay extends Component {
} }
async componentDidMount() { async componentDidMount() {
console.log("Initializing..."); console.log("Initializing...");
this.#_beatSaverURL = document.location.origin + "/api/beatsaver/map?hash=%s"; this.#_beatSaverURL =
document.location.origin + "/api/beatsaver/map?hash=%s";
const urlSearchParams = new URLSearchParams(window.location.search); const urlSearchParams = new URLSearchParams(window.location.search);
const params = Object.fromEntries(urlSearchParams.entries()); const params = Object.fromEntries(urlSearchParams.entries());
// Check what website the player wants to use // Check what website the player wants to use
if (params.beatleader === 'true') { if (params.beatleader === "true") {
this.setState({ websiteType: "BeatLeader" }); this.setState({ websiteType: "BeatLeader" });
} }
const id = params.id; const id = params.id;
if (!id) { // Check if the id param is valid if (!id) {
// Check if the id param is valid
this.setState({ loading: false, isValidSteamId: false }); this.setState({ loading: false, isValidSteamId: false });
return; return;
} }
// Check if the player wants to disable their stats (pp, global pos, etc) // Check if the player wants to disable their stats (pp, global pos, etc)
if (params.showPlayerStats === 'false' || params.playerstats === 'false') { if (params.showPlayerStats === "false" || params.playerstats === "false") {
this.setState({ showPlayerStats: false }); this.setState({ showPlayerStats: false });
} }
@ -114,18 +115,18 @@ export default class Overlay extends Component {
let shouldConnectSocket = false; let shouldConnectSocket = false;
// Check if the player wants to show their current score information // Check if the player wants to show their current score information
if (params.showScoreInfo === 'true' || params.scoreinfo === 'true') { if (params.showScoreInfo === "true" || params.scoreinfo === "true") {
this.setState({ showScore: true }); this.setState({ showScore: true });
shouldConnectSocket = true; shouldConnectSocket = true;
} }
// Check if the player wants to show the current song // Check if the player wants to show the current song
if (params.showSongInfo === 'true' || params.songinfo === 'true') { if (params.showSongInfo === "true" || params.songinfo === "true") {
this.setState({ showSongInfo: true }); this.setState({ showSongInfo: true });
shouldConnectSocket = true; shouldConnectSocket = true;
} }
// Mainly used for the preview // Mainly used for the preview
if (params.textColor) { if (params.textColor) {
this.setState({ textColor: params.textColor }); this.setState({ textColor: params.textColor });
} }
@ -143,11 +144,17 @@ export default class Overlay extends Component {
* @returns * @returns
*/ */
async updateData(id) { async updateData(id) {
const data = await fetch(new Utils().getWebsiteApi(id == "test" ? "Test" : this.state.websiteType).ApiUrl.replace("%s", id), { const data = await fetch(
mode: 'cors' new Utils()
}); .getWebsiteApi(id == "test" ? "Test" : this.state.websiteType)
.ApiUrl.replace("%s", id),
{
mode: "cors",
}
);
const json = await data.json(); const json = await data.json();
if (json.errorMessage) { // Invalid account if (json.errorMessage) {
// Invalid account
this.setState({ loading: false, isValidSteamId: false }); this.setState({ loading: false, isValidSteamId: false });
return; return;
} }
@ -158,7 +165,10 @@ export default class Overlay extends Component {
* Setup the HTTP Status connection * Setup the HTTP Status connection
*/ */
connectSocket(socketAddress) { connectSocket(socketAddress) {
socketAddress = (socketAddress === undefined ? 'ws://localhost' : `ws://${socketAddress}`) + ":6557/socket"; socketAddress =
(socketAddress === undefined
? "ws://localhost"
: `ws://${socketAddress}`) + ":6557/socket";
if (this.state.isConnectedToSocket) return; if (this.state.isConnectedToSocket) return;
if (this.state.isVisible) { if (this.state.isVisible) {
@ -167,24 +177,26 @@ export default class Overlay extends Component {
console.log(`Connecting to ${socketAddress}`); console.log(`Connecting to ${socketAddress}`);
const socket = new WebSocket(socketAddress); const socket = new WebSocket(socketAddress);
socket.addEventListener('open', () => { socket.addEventListener("open", () => {
console.log(`Connected to ${socketAddress}`); console.log(`Connected to ${socketAddress}`);
this.setState({ isConnectedToSocket: true }); this.setState({ isConnectedToSocket: true });
}); });
socket.addEventListener('close', () => { socket.addEventListener("close", () => {
console.log("Attempting to re-connect to the HTTP Status socket in 10 seconds."); console.log(
"Attempting to re-connect to the HTTP Status socket in 10 seconds."
);
this.setState({ isConnectedToSocket: false }); this.setState({ isConnectedToSocket: false });
setTimeout(() => this.connectSocket(), 10_000); setTimeout(() => this.connectSocket(), 10_000);
}); });
socket.addEventListener('message', (message) => { socket.addEventListener("message", (message) => {
const json = JSON.parse(message.data); const json = JSON.parse(message.data);
this.handleCurrentSongTime(json) this.handleCurrentSongTime(json);
if (!this.handlers[json.event]) { if (!this.handlers[json.event]) {
console.log("Unhandled message from HTTP Status. (" + json.event + ")"); console.log("Unhandled message from HTTP Status. (" + json.event + ")");
return; return;
} }
this.handlers[json.event](json || []); this.handlers[json.event](json || []);
}) });
this.setState({ socket: socket }); this.setState({ socket: socket });
} }
@ -194,10 +206,12 @@ export default class Overlay extends Component {
* @param {[]} songData * @param {[]} songData
*/ */
async setBeatSaver(songData) { async setBeatSaver(songData) {
console.log("Updating BeatSaver info") console.log("Updating BeatSaver info");
const data = await fetch(this.#_beatSaverURL.replace("%s", songData.levelId)); const data = await fetch(
this.#_beatSaverURL.replace("%s", songData.levelId)
);
const json = await data.json(); const json = await data.json();
this.setState({ beatSaverData: json }) this.setState({ beatSaverData: json });
} }
/** /**
@ -211,59 +225,68 @@ export default class Overlay extends Component {
}, 250); }, 250);
this.setState({ this.setState({
leftHand: { leftHand: {
averageCut: [15.00], averageCut: [15.0],
averagePreSwing: [70.00], averagePreSwing: [70.0],
averagePostSwing: [30.00], averagePostSwing: [30.0],
}, },
rightHand: { rightHand: {
averageCut: [15.00], averageCut: [15.0],
averagePreSwing: [70.00], averagePreSwing: [70.0],
averagePostSwing: [30.00], averagePostSwing: [30.0],
}, },
songInfo: undefined, songInfo: undefined,
beatSaverData: undefined, beatSaverData: undefined,
currentSongTime: 0, currentSongTime: 0,
currentScore: 0, currentScore: 0,
percentage: "100.00%", percentage: "100.00%",
isVisible: visible isVisible: visible,
}); });
} }
// The HTTP Status handlers // The HTTP Status handlers
handlers = { handlers = {
"hello": (data) => { hello: (data) => {
console.log("Hello from HTTP Status!"); console.log("Hello from HTTP Status!");
if (data.status) { if (data.status) {
this.setState({songData: data}); this.setState({ songData: data });
if (data.status.beatmap) { if (data.status.beatmap) {
this.setBeatSaver(data.status.beatmap); this.setBeatSaver(data.status.beatmap);
} }
} }
}, },
"scoreChanged": (data) => { scoreChanged: (data) => {
const { status } = data; const { status } = data;
const { score, currentMaxScore } = status.performance; const { score, currentMaxScore } = status.performance;
const percent = currentMaxScore > 0 ? ((score / currentMaxScore) * 1000 / 10).toFixed(2) : 0.00; const percent =
currentMaxScore > 0
? (((score / currentMaxScore) * 1000) / 10).toFixed(2)
: 0.0;
this.setState({ this.setState({
currentScore: score, currentScore: score,
percentage: this.state.failed ? percent * 2 : percent + "%" percentage: this.state.failed ? percent * 2 : percent + "%",
}) });
}, },
"noteFullyCut": (data) => { noteFullyCut: (data) => {
const { noteCut } = data; const { noteCut } = data;
console.log(noteCut) console.log(noteCut);
// Left Saber // Left Saber
if (noteCut.saberType === 'SaberA') { if (noteCut.saberType === "SaberA") {
const data = this.state.leftHand; const data = this.state.leftHand;
if (data.averageCut.includes(15) && data.averageCut.length === 1) { if (data.averageCut.includes(15) && data.averageCut.length === 1) {
data.averageCut = []; data.averageCut = [];
} }
if (data.averagePreSwing.includes(70) && data.averagePreSwing.length === 1) { if (
data.averagePreSwing.includes(70) &&
data.averagePreSwing.length === 1
) {
data.averagePreSwing = []; data.averagePreSwing = [];
} }
if (data.averagePostSwing.includes(30) && data.averagePostSwing.length === 1) { if (
data.averagePostSwing.includes(30) &&
data.averagePostSwing.length === 1
) {
data.averagePostSwing = []; data.averagePostSwing = [];
} }
data.averagePreSwing.push(noteCut.beforeSwingRating * 70); data.averagePreSwing.push(noteCut.beforeSwingRating * 70);
@ -273,15 +296,21 @@ export default class Overlay extends Component {
} }
// Left Saber // Left Saber
if (noteCut.saberType === 'SaberB') { if (noteCut.saberType === "SaberB") {
const data = this.state.rightHand; const data = this.state.rightHand;
if (data.averageCut.includes(15) && data.averageCut.length === 1) { if (data.averageCut.includes(15) && data.averageCut.length === 1) {
data.averageCut = []; data.averageCut = [];
} }
if (data.averagePreSwing.includes(70) && data.averagePreSwing.length === 1) { if (
data.averagePreSwing.includes(70) &&
data.averagePreSwing.length === 1
) {
data.averagePreSwing = []; data.averagePreSwing = [];
} }
if (data.averagePostSwing.includes(30) && data.averagePostSwing.length === 1) { if (
data.averagePostSwing.includes(30) &&
data.averagePostSwing.length === 1
) {
data.averagePostSwing = []; data.averagePostSwing = [];
} }
data.averagePreSwing.push(noteCut.beforeSwingRating * 70); data.averagePreSwing.push(noteCut.beforeSwingRating * 70);
@ -290,77 +319,85 @@ export default class Overlay extends Component {
this.setState({ rightHand: data }); this.setState({ rightHand: data });
} }
}, },
"songStart": (data) => { songStart: (data) => {
console.log("Going into level, resetting data.") console.log("Going into level, resetting data.");
this.resetData(true); this.resetData(true);
this.setState({ songData: data, paused: false }) this.setState({ songData: data, paused: false });
this.setBeatSaver(data.status.beatmap); this.setBeatSaver(data.status.beatmap);
}, },
"finished": () => { finished: () => {
this.resetData(false); this.resetData(false);
}, },
"softFail": () => { softFail: () => {
this.setState({ failed: true }); this.setState({ failed: true });
}, },
"pause": () => { pause: () => {
this.setState({ paused: true }); this.setState({ paused: true });
}, },
"resume": () => { resume: () => {
this.setState({ paused: false }); this.setState({ paused: false });
}, },
"menu": () => { menu: () => {
this.resetData(false); this.resetData(false);
}, },
"noteCut": () => {}, noteCut: () => {},
"noteMissed": () => {}, noteMissed: () => {},
"noteSpawned": () => {}, noteSpawned: () => {},
"bombMissed": () => {}, bombMissed: () => {},
"beatmapEvent": () => {}, beatmapEvent: () => {},
"energyChanged": () => {}, energyChanged: () => {},
} };
render() { render() {
const { loading, isValidSteamId, data, websiteType } = this.state; const { loading, isValidSteamId, data, websiteType } = this.state;
if (this.state.textColor !== undefined) { if (this.state.textColor !== undefined) {
const element = document.querySelector("." + styles.main); const element = document.querySelector("." + styles.main);
element.style.color = this.state.textColor element.style.color = this.state.textColor;
} }
return <div className={styles.main}> return (
{ loading ? <div className={styles.main}>
<div className={styles.loading}> {loading ? (
<h2>Loading...</h2> <div className={styles.loading}>
<h2>Loading...</h2>
</div>
) : !isValidSteamId ? (
<div className={styles.invalidPlayer}>
<h1>Invalid player, please visit the main page.</h1>
<Link href="/">
<a>Go Home</a>
</Link>
</div>
) : (
<div className={styles.overlay}>
{this.state.showPlayerStats ? (
<PlayerStats
pp={data.pp.toLocaleString()}
globalPos={data.rank.toLocaleString()}
country={data.country}
countryRank={data.countryRank.toLocaleString()}
websiteType={websiteType}
avatar={data.profilePicture || data.avatar}
/>
) : (
<></>
)}
{this.state.showScore && this.state.isVisible ? (
<ScoreStats data={this.state} />
) : (
<></>
)}
{this.state.showSongInfo &&
this.state.beatSaverData !== undefined &&
this.state.isVisible ? (
<SongInfo data={this.state} />
) : (
<></>
)}
</div>
)}
</div> </div>
: !isValidSteamId ? );
<div className={styles.invalidPlayer}>
<h1>Invalid player, please visit the main page.</h1>
<Link href="/">
<a>Go Home</a>
</Link>
</div> :
<div className={styles.overlay}>
{
this.state.showPlayerStats ? <PlayerStats
pp={data.pp.toLocaleString()}
globalPos={data.rank.toLocaleString()}
country={data.country}
countryRank={data.countryRank.toLocaleString()}
websiteType={websiteType}
avatar={data.profilePicture || data.avatar}
/>
: <></>
}
{
this.state.showScore && this.state.isVisible ?
<ScoreStats data={this.state} /> : <></>
}
{
this.state.showSongInfo && this.state.beatSaverData !== undefined && this.state.isVisible ?
<SongInfo data={this.state}/> : <></>
}
</div>
}
</div>
} }
} }

@ -1,21 +1,23 @@
import Image from "next/image"; import Image from "next/image";
import styles from '../../styles/avatar.module.css'; import styles from "../../styles/avatar.module.css";
const Avatar = (props) => { const Avatar = (props) => {
return <> return (
<Image <>
className={styles.playerAvatar} <Image
src={props.url} className={styles.playerAvatar}
width={180} src={props.url}
height={180} width={180}
alt={'Avatar image'} height={180}
loading='lazy' alt={"Avatar image"}
placeholder="blur" loading="lazy"
blurDataURL="https://cdn.fascinated.cc/MhCUeHZLsh.webp?raw=true" placeholder="blur"
unoptimized={true} blurDataURL="https://cdn.fascinated.cc/MhCUeHZLsh.webp?raw=true"
/> unoptimized={true}
</> />
} </>
);
};
export default Avatar; export default Avatar;

@ -1,13 +1,15 @@
import { Navbar, Text } from "@nextui-org/react"; import { Navbar, Text } from "@nextui-org/react";
const NavBar = () => { const NavBar = () => {
return <Navbar isBordered variant={"sticky"}> return (
<Navbar.Brand> <Navbar isBordered variant={"sticky"}>
<Text b color="inherit"> <Navbar.Brand>
BeatSaber Overlay <Text b color="inherit">
</Text> BeatSaber Overlay
</Navbar.Brand> </Text>
</Navbar> </Navbar.Brand>
} </Navbar>
);
};
export default NavBar; export default NavBar;

@ -1,24 +1,37 @@
import ReactCountryFlag from "react-country-flag"; import ReactCountryFlag from "react-country-flag";
import styles from '../../styles/playerStats.module.css'; import styles from "../../styles/playerStats.module.css";
import Avatar from "./Avatar"; import Avatar from "./Avatar";
const PlayerStats = (props) => { const PlayerStats = (props) => {
return <div className={styles.playerStatsContainer}> return (
<div> <div className={styles.playerStatsContainer}>
<Avatar url={props.avatar} /> <div>
</div> <Avatar url={props.avatar} />
<div className={styles.playerStats}> </div>
<p>{props.pp}pp <span style={{ <div className={styles.playerStats}>
fontSize: '20px', <p>
}}>({props.websiteType})</span></p> {props.pp}pp{" "}
<p>#{props.globalPos}</p> <span
<div className={styles.playerCountry}> style={{
<p>#{props.countryRank}</p> fontSize: "20px",
<ReactCountryFlag className={styles.playerCountryIcon} svg countryCode={props.country} /> }}
</div> >
</div> ({props.websiteType})
</div> </span>
} </p>
<p>#{props.globalPos}</p>
<div className={styles.playerCountry}>
<p>#{props.countryRank}</p>
<ReactCountryFlag
className={styles.playerCountryIcon}
svg
countryCode={props.country}
/>
</div>
</div>
</div>
);
};
export default PlayerStats; export default PlayerStats;

@ -1,44 +1,45 @@
import {Component} from "react"; import { Component } from "react";
import styles from '../../styles/scoreStats.module.css'; import styles from "../../styles/scoreStats.module.css";
export default class ScoreStats extends Component { export default class ScoreStats extends Component {
constructor(params) {
super(params);
}
constructor(params) { /**
super(params); * Returns the average of the provided numbers list
} *
* @param {List<Number>} hitValues
* @returns The average value
*/
getAverage(hitValues) {
return hitValues.reduce((p, c) => p + c, 0) / hitValues.length;
}
/** render() {
* Returns the average of the provided numbers list const data = this.props.data;
*
* @param {List<Number>} hitValues
* @returns The average value
*/
getAverage(hitValues) {
return hitValues.reduce((p, c) => p + c, 0) / hitValues.length;
}
render() { return (
const data = this.props.data; <div className={styles.scoreStats}>
<div className={styles.scoreStatsInfo}>
return <div className={styles.scoreStats}> <p>{data.percentage}</p>
<div className={styles.scoreStatsInfo}> <p>{data.currentScore.toLocaleString()}</p>
<p>{data.percentage}</p> </div>
<p>{data.currentScore.toLocaleString()}</p> <p className={styles.scoreStatsAverageCut}>Average Cut</p>
</div> <div className={styles.scoreStatsHands}>
<p className={styles.scoreStatsAverageCut}>Average Cut</p> <div className={styles.scoreStatsLeft}>
<div className={styles.scoreStatsHands}> <p>{this.getAverage(data.leftHand.averagePreSwing).toFixed(2)}</p>
<div className={styles.scoreStatsLeft}> <p>{this.getAverage(data.leftHand.averagePostSwing).toFixed(2)}</p>
<p>{this.getAverage(data.leftHand.averagePreSwing).toFixed(2)}</p> <p>{this.getAverage(data.leftHand.averageCut).toFixed(2)}</p>
<p>{this.getAverage(data.leftHand.averagePostSwing).toFixed(2)}</p> </div>
<p>{this.getAverage(data.leftHand.averageCut).toFixed(2)}</p> <div className={styles.scoreStatsRight}>
</div> <p>{this.getAverage(data.rightHand.averagePreSwing).toFixed(2)}</p>
<div className={styles.scoreStatsRight}> <p>{this.getAverage(data.rightHand.averagePostSwing).toFixed(2)}</p>
<p>{this.getAverage(data.rightHand.averagePreSwing).toFixed(2)}</p> <p>{this.getAverage(data.rightHand.averageCut).toFixed(2)}</p>
<p>{this.getAverage(data.rightHand.averagePostSwing).toFixed(2)}</p> </div>
<p>{this.getAverage(data.rightHand.averageCut).toFixed(2)}</p> </div>
</div> </div>
</div> );
</div> }
}
} }

@ -1,84 +1,99 @@
import {Component} from "react"; import { Component } from "react";
import styles from '../../styles/songInfo.module.css'; import styles from "../../styles/songInfo.module.css";
export default class SongInfo extends Component { export default class SongInfo extends Component {
constructor(params) {
super(params);
this.state = {
diffColor: undefined,
};
}
constructor(params) { componentDidMount() {
super(params); const data = this.props.data.songData.status.beatmap;
this.state = { this.formatDiff(data.difficulty);
diffColor: undefined }
}
}
componentDidMount() { /**
const data = this.props.data.songData.status.beatmap; * Update the difficulity color from the given difficulity
this.formatDiff(data.difficulty); *
} * @param {string} diff
*/
formatDiff(diff) {
if (diff === "Expert+") {
this.setState({ diffColor: "#8f48db" });
}
if (diff === "Expert") {
this.setState({ diffColor: "#bf2a42" });
}
if (diff === "Hard") {
this.setState({ diffColor: "tomato" });
}
if (diff === "Normal") {
this.setState({ diffColor: "#59b0f4" });
}
if (diff === "Easy") {
this.setState({ diffColor: "MediumSeaGreen" });
}
}
/** /**
* Update the difficulity color from the given difficulity * Format the given ms
* *
* @param {string} diff * @param {Number} millis
*/ * @returns The formatted time
formatDiff(diff) { */
if (diff === "Expert+") { msToMinSeconds(millis) {
this.setState({ diffColor: "#8f48db" }); const minutes = Math.floor(millis / 60000);
} const seconds = Number(((millis % 60000) / 1000).toFixed(0));
if (diff === "Expert") { return seconds === 60
this.setState({ diffColor: "#bf2a42" }); ? minutes + 1 + ":00"
} : minutes + ":" + (seconds < 10 ? "0" : "") + seconds;
if (diff === "Hard") { }
this.setState({ diffColor: "tomato" });
}
if (diff === "Normal") {
this.setState({ diffColor: "#59b0f4" });
}
if (diff === "Easy") {
this.setState({ diffColor: "MediumSeaGreen" });
}
}
/** render() {
* Format the given ms const data = this.props.data.songData.status.beatmap;
* const beatSaverData = this.props.data.beatSaverData.data;
* @param {Number} millis const songArt = beatSaverData.songArt;
* @returns The formatted time const bsr = beatSaverData.bsr;
*/ const { songName, songAuthorName, difficulty } = data;
msToMinSeconds(millis) { // what in the fuck is this?? LMFAO
const minutes = Math.floor(millis / 60000); const songTimerPercentage =
const seconds = Number(((millis % 60000) / 1000).toFixed(0)); (this.props.data.currentSongTime / 1000 / (data.length / 1000)) * 100000;
return seconds === 60 ? minutes + 1 + ":00" : minutes + ":" + (seconds < 10 ? "0" : "") + seconds;
}
render() { return (
const data = this.props.data.songData.status.beatmap; <div className={styles.songInfoContainer}>
const beatSaverData = this.props.data.beatSaverData.data; <img src={songArt} />
const songArt = beatSaverData.songArt; <div className={styles.songInfo}>
const bsr = beatSaverData.bsr; <p className={styles.songInfoSongName}>
const { {songName.length > 35
songName, ? songName.substring(0, 35) + "..."
songAuthorName, : songName}
difficulty </p>
} = data <p className={styles.songInfoSongAuthor}>{songAuthorName}</p>
// what in the fuck is this?? LMFAO <div className={styles.songInfoSongOtherContainer}>
const songTimerPercentage = ((this.props.data.currentSongTime / 1000) / (data.length / 1000)) * 100000; <p
className={styles.songInfoDiff}
return <div className={styles.songInfoContainer}> style={{ backgroundColor: this.state.diffColor }}
<img src={songArt}/> >
<div className={styles.songInfo}> {difficulty}
<p className={styles.songInfoSongName}>{songName.length > 35 ? songName.substring(0, 35) + "..." : songName}</p> </p>
<p className={styles.songInfoSongAuthor}>{songAuthorName}</p> <p className={styles.songInfoBsr}>!bsr {bsr}</p>
<div className={styles.songInfoSongOtherContainer}> </div>
<p className={styles.songInfoDiff} style={{ backgroundColor: this.state.diffColor }}>{difficulty}</p> <p className={styles.songTimeText}>
<p className={styles.songInfoBsr}>!bsr {bsr}</p> {this.msToMinSeconds(this.props.data.currentSongTime * 1000)}/
</div> {this.msToMinSeconds(data.length)}
<p className={styles.songTimeText}>{this.msToMinSeconds(this.props.data.currentSongTime * 1000)}/{this.msToMinSeconds(data.length)}</p> </p>
<div className={styles.songTimeContainer}> <div className={styles.songTimeContainer}>
<div className={styles.songTimeBackground}/> <div className={styles.songTimeBackground} />
<div className={styles.songTime} style={{ width: songTimerPercentage + '%' }}/> <div
</div> className={styles.songTime}
</div> style={{ width: songTimerPercentage + "%" }}
</div> />
} </div>
</div>
</div>
);
}
} }

@ -1,15 +1,15 @@
import Config from '../../config.json'; import Config from "../../config.json";
const WebsiteTypes = { const WebsiteTypes = {
ScoreSaber: { ScoreSaber: {
ApiUrl: Config.proxy_url + "/https://scoresaber.com/api/player/%s/full" ApiUrl: Config.proxy_url + "/https://scoresaber.com/api/player/%s/full",
}, },
BeatLeader: { BeatLeader: {
ApiUrl: Config.proxy_url + "/https://api.beatleader.xyz/player/%s" ApiUrl: Config.proxy_url + "/https://api.beatleader.xyz/player/%s",
}, },
Test: { Test: {
ApiUrl: "/api/mockdata" ApiUrl: "/api/mockdata",
} },
} };
export default WebsiteTypes export default WebsiteTypes;

@ -1,15 +1,15 @@
import WebsiteTypes from "../consts/WebsiteType"; import WebsiteTypes from "../consts/WebsiteType";
export default class Utils { export default class Utils {
constructor() {}; constructor() {}
/** /**
* Returns the information for the given website type. * Returns the information for the given website type.
* *
* @param {WebsiteTypes} website * @param {WebsiteTypes} website
* @returns The website type's information. * @returns The website type's information.
*/ */
getWebsiteApi(website) { getWebsiteApi(website) {
return WebsiteTypes[website] return WebsiteTypes[website];
} }
} }

@ -1,3 +1,5 @@
*, html, body { *,
html,
body {
background-color: transparent; background-color: transparent;
} }

@ -1,5 +1,5 @@
.main { .main {
font-family: 'Roboto', sans-serif !important; font-family: "Roboto", sans-serif !important;
color: white; color: white;
font-size: xx-large; font-size: xx-large;
line-height: 1.4em !important; line-height: 1.4em !important;

@ -1,12 +1,12 @@
.main { .main {
font-family: 'Teko', sans-serif !important; font-family: "Teko", sans-serif !important;
color: white; color: white;
font-size: xx-large; font-size: xx-large;
line-height: 1.4em !important; line-height: 1.4em !important;
} }
.main p { .main p {
font-family: 'Teko', sans-serif !important; font-family: "Teko", sans-serif !important;
color: white; color: white;
letter-spacing: normal; letter-spacing: normal;
} }

@ -1,8 +1,8 @@
.scoreStats { .scoreStats {
text-align: center; text-align: center;
position:absolute; position: absolute;
top:0; top: 0;
right:0; right: 0;
margin-right: 5px; margin-right: 5px;
min-width: 135px; min-width: 135px;
} }

@ -1,8 +1,8 @@
.songInfoContainer { .songInfoContainer {
display: flex; display: flex;
position: fixed; position: fixed;
bottom:0; bottom: 0;
left:0; left: 0;
margin-left: 5px; margin-left: 5px;
margin-bottom: 5px; margin-bottom: 5px;
} }

@ -1,35 +1,37 @@
import Config from '../config.json'; import Config from "../config.json";
const mapCache = new Map(); const mapCache = new Map();
module.exports = { module.exports = {
BEATSAVER_MAP_API: Config.proxy_url + "/https://api.beatsaver.com/maps/hash/%s", BEATSAVER_MAP_API:
Config.proxy_url + "/https://api.beatsaver.com/maps/hash/%s",
/** /**
* Gets a specified maps data from BeatSaver * Gets a specified maps data from BeatSaver
* *
* @param {string} hash * @param {string} hash
* @returns The map data * @returns The map data
*/ */
async getMapData(hash) { async getMapData(hash) {
hash = this.BEATSAVER_MAP_API.replace("%s", hash); hash = this.BEATSAVER_MAP_API.replace("%s", hash);
if (mapCache.has(hash)) { // Return from cache if (mapCache.has(hash)) {
return mapCache.get(hash); // Return from cache
} return mapCache.get(hash);
}
const data = await fetch(hash, { const data = await fetch(hash, {
headers: { headers: {
"origin": "Fascinated Overlay" origin: "Fascinated Overlay",
} },
}); });
if (data.status === 404) { if (data.status === 404) {
return undefined; return undefined;
} }
const json = await data.json(); const json = await data.json();
mapCache.set(hash, json); mapCache.set(hash, json);
setTimeout(() => { setTimeout(() => {
mapCache.delete(hash); mapCache.delete(hash);
}, 60 * 60 * 1000); // 1h }, 60 * 60 * 1000); // 1h
return json; return json;
} },
} };