Refactored like the entire frontend
This commit is contained in:
parent
3e56a16b14
commit
b89f31a96e
@ -10,6 +10,7 @@ const nextConfig = {
|
|||||||
"cdn.scoresaber.com",
|
"cdn.scoresaber.com",
|
||||||
"eu.cdn.beatsaver.com",
|
"eu.cdn.beatsaver.com",
|
||||||
"cdn.fascinated.cc",
|
"cdn.fascinated.cc",
|
||||||
|
"avatars.akamai.steamstatic.com",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
10257
package-lock.json
generated
Normal file
10257
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -12,6 +12,7 @@
|
|||||||
"@emotion/cache": "^11.10.5",
|
"@emotion/cache": "^11.10.5",
|
||||||
"@emotion/server": "^11.10.0",
|
"@emotion/server": "^11.10.0",
|
||||||
"@nextui-org/react": "^1.0.0-beta.10",
|
"@nextui-org/react": "^1.0.0-beta.10",
|
||||||
|
"axios": "^1.1.3",
|
||||||
"core-js-pure": "^3.26.0",
|
"core-js-pure": "^3.26.0",
|
||||||
"critters": "^0.0.16",
|
"critters": "^0.0.16",
|
||||||
"ioredis": "^5.2.3",
|
"ioredis": "^5.2.3",
|
||||||
@ -24,11 +25,14 @@
|
|||||||
"react-country-flag": "^3.0.2",
|
"react-country-flag": "^3.0.2",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-toastify": "^9.0.8",
|
"react-toastify": "^9.0.8",
|
||||||
"sharp": "^0.31.1"
|
"sharp": "^0.31.1",
|
||||||
|
"websocket": "^1.0.34",
|
||||||
|
"zustand": "^4.1.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^18.11.7",
|
"@types/node": "^18.11.7",
|
||||||
"@types/react": "^18.0.24",
|
"@types/react": "^18.0.24",
|
||||||
|
"@types/websocket": "^1.0.5",
|
||||||
"eslint": "8.26.0",
|
"eslint": "8.26.0",
|
||||||
"eslint-config-next": "12.3.1",
|
"eslint-config-next": "12.3.1",
|
||||||
"typescript": "^4.8.4"
|
"typescript": "^4.8.4"
|
||||||
|
@ -1 +1 @@
|
|||||||
window.__ENV = {"REACT_APP_HTTP_PROXY":"https://proxy.fascinated.cc","REACT_APP_SITE_COLOR":"0EBFE9","REACT_APP_SITE_DESCRIPTION":"Free, simple, and easy to use beat saber overlay for OBS","REACT_APP_SITE_NAME":"BeatSaber Overlay","REACT_APP_SITE_TITLE":"BeatSaber Overlay - Simple and easy to use BeatSaber overlay","REACT_APP_SITE_URL":"https://bs-overlay.fascinated.cc"};
|
window.__ENV = {"REACT_APP_HTTP_PROXY":"https://proxy.fascinated.cc","REACT_APP_SITE_COLOR":"0EBFE9","REACT_APP_SITE_DESCRIPTION":"Free, simple, and easy to use beat saber overlay for OBS","REACT_APP_SITE_NAME":"BeatSaber Overlay","REACT_APP_SITE_TITLE":"BeatSaber Overlay - Simple and easy to use BeatSaber overlay","REACT_APP_SITE_URL":"http://localhost:3000"};
|
@ -1,35 +1,70 @@
|
|||||||
import ReactCountryFlag from "react-country-flag";
|
import ReactCountryFlag from "react-country-flag";
|
||||||
|
import { useDataStore } from "../store/overlayDataStore";
|
||||||
import styles from "../styles/playerStats.module.css";
|
import { useSettingsStore } from "../store/overlaySettingsStore";
|
||||||
|
import { usePlayerDataStore } from "../store/playerDataStore";
|
||||||
import Avatar from "./Avatar";
|
import Avatar from "./Avatar";
|
||||||
|
|
||||||
const PlayerStats = (props) => {
|
import { useSongDataStore } from "../store/songDataStore";
|
||||||
|
import styles from "../styles/playerStats.module.css";
|
||||||
|
|
||||||
|
const PlayerStats = () => {
|
||||||
|
const [showPlayerStats, shouldReplacePlayerInfoWithScore] = useSettingsStore(
|
||||||
|
(store) => [store.showPlayerStats, store.shouldReplacePlayerInfoWithScore]
|
||||||
|
);
|
||||||
|
const inSong = useSongDataStore((state) => state.inSong);
|
||||||
|
const [loadedDuringSong] = useDataStore((state) => [state.loadedDuringSong]);
|
||||||
|
const [leaderboardType] = useSettingsStore((state) => [
|
||||||
|
state.leaderboardType,
|
||||||
|
]);
|
||||||
|
const [isLoading, pp, avatar, globalPos, countryRank, country] =
|
||||||
|
usePlayerDataStore((state) => [
|
||||||
|
state.isLoading,
|
||||||
|
state.pp,
|
||||||
|
state.avatar,
|
||||||
|
state.globalPos,
|
||||||
|
state.countryRank,
|
||||||
|
state.country,
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!showPlayerStats) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Checks if we are in a song and should replace the player info with song info
|
||||||
|
if (shouldReplacePlayerInfoWithScore && inSong) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return <div className={styles.playerStatsContainer}>Loading...</div>;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.playerStatsContainer}>
|
<div className={styles.playerStatsContainer}>
|
||||||
<div>
|
<div>
|
||||||
<Avatar url={props.avatar} />
|
<Avatar url={avatar} />
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.playerStats}>
|
<div className={styles.playerStats}>
|
||||||
<p>
|
<p>
|
||||||
{props.pp}pp{" "}
|
{pp}pp{" "}
|
||||||
<span
|
<span
|
||||||
style={{
|
style={{
|
||||||
fontSize: "23px",
|
fontSize: "23px",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
({props.websiteType})
|
({leaderboardType})
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
<p>#{props.globalPos}</p>
|
<p>#{globalPos}</p>
|
||||||
<div className={styles.playerCountry}>
|
<div className={styles.playerCountry}>
|
||||||
<p>#{props.countryRank}</p>
|
<p>#{countryRank}</p>
|
||||||
<ReactCountryFlag
|
<ReactCountryFlag
|
||||||
className={styles.playerCountryIcon}
|
className={styles.playerCountryIcon}
|
||||||
svg
|
svg
|
||||||
countryCode={props.country}
|
countryCode={country}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{props.loadedDuringSong ? (
|
{loadedDuringSong ? (
|
||||||
<>
|
<>
|
||||||
<p className={styles.connectedDuringSong}>
|
<p className={styles.connectedDuringSong}>
|
||||||
Connected during song, some data
|
Connected during song, some data
|
||||||
|
@ -1,60 +1,47 @@
|
|||||||
import { Component } from "react";
|
import { useSettingsStore } from "../store/overlaySettingsStore";
|
||||||
|
import { useSongDataStore } from "../store/songDataStore";
|
||||||
import styles from "../styles/scoreStats.module.css";
|
import styles from "../styles/scoreStats.module.css";
|
||||||
import Utils from "../utils/utils";
|
|
||||||
|
|
||||||
export default class ScoreStats extends Component {
|
export default function ScoreStats() {
|
||||||
constructor(params) {
|
const [showScoreInfo] = useSettingsStore((store) => [store.showScoreInfo]);
|
||||||
super(params);
|
const [percentage, currentScore, currentPP, saberA, saberB, isLoading] =
|
||||||
this.lastKnownPP = undefined;
|
useSongDataStore((store) => [
|
||||||
|
store.percentage,
|
||||||
|
store.currentScore,
|
||||||
|
store.currentPP,
|
||||||
|
store.saberA,
|
||||||
|
store.saberB,
|
||||||
|
store.isLoading,
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
if (!showScoreInfo) {
|
||||||
* Returns the average of the provided numbers list
|
return null;
|
||||||
*
|
|
||||||
* @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}>
|
||||||
let currentPP = Utils.calculatePP(
|
<div className={styles.scoreStatsInfo}>
|
||||||
data.mapStarCount,
|
<p>{percentage}</p>
|
||||||
data.percentage.replace("%", ""),
|
<p>{currentScore.toLocaleString()}</p>
|
||||||
data.websiteType
|
{currentPP !== undefined ? <p>{currentPP.toFixed(0)}pp</p> : null}
|
||||||
);
|
</div>
|
||||||
if (this.lastKnownPP === undefined) {
|
<p className={styles.scoreStatsAverageCut}>Average Cut</p>
|
||||||
this.lastKnownPP = currentPP;
|
<div className={styles.scoreStatsHands}>
|
||||||
}
|
<div className={styles.scoreStatsLeft}>
|
||||||
if (currentPP === undefined) {
|
<p>{saberA.averagePreSwing.toFixed(2)}</p>
|
||||||
currentPP = this.lastKnownPP;
|
<p>{saberA.averagePostSwing.toFixed(2)}</p>
|
||||||
}
|
<p>{saberA.cutDistanceScore.toFixed(2)}</p>
|
||||||
this.lastKnownPP = currentPP;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={styles.scoreStats}>
|
|
||||||
<div className={styles.scoreStatsInfo}>
|
|
||||||
<p>{data.percentage}</p>
|
|
||||||
<p>{data.currentScore.toLocaleString()}</p>
|
|
||||||
{currentPP !== undefined ? <p>{currentPP.toFixed(0)}pp</p> : null}
|
|
||||||
</div>
|
</div>
|
||||||
<p className={styles.scoreStatsAverageCut}>Average Cut</p>
|
<div className={styles.scoreStatsRight}>
|
||||||
<div className={styles.scoreStatsHands}>
|
<p>{saberB.averagePreSwing.toFixed(2)}</p>
|
||||||
<div className={styles.scoreStatsLeft}>
|
<p>{saberB.averagePostSwing.toFixed(2)}</p>
|
||||||
<p>{data.SaberA.averagePreSwing.toFixed(2)}</p>
|
<p>{saberB.cutDistanceScore.toFixed(2)}</p>
|
||||||
<p>{data.SaberA.averagePostSwing.toFixed(2)}</p>
|
|
||||||
<p>{data.SaberA.cutDistanceScore.toFixed(2)}</p>
|
|
||||||
</div>
|
|
||||||
<div className={styles.scoreStatsRight}>
|
|
||||||
<p>{data.SaberB.averagePreSwing.toFixed(2)}</p>
|
|
||||||
<p>{data.SaberB.averagePostSwing.toFixed(2)}</p>
|
|
||||||
<p>{data.SaberB.cutDistanceScore.toFixed(2)}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
</div>
|
||||||
}
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,118 +1,125 @@
|
|||||||
import Image from "next/future/image";
|
import Image from "next/future/image";
|
||||||
import { Component } from "react";
|
import { useSettingsStore } from "../store/overlaySettingsStore";
|
||||||
|
import { useSongDataStore } from "../store/songDataStore";
|
||||||
|
|
||||||
import styles from "../styles/songInfo.module.css";
|
import styles from "../styles/songInfo.module.css";
|
||||||
|
|
||||||
export default class SongInfo extends Component {
|
/**
|
||||||
constructor(params) {
|
* Format the given ms
|
||||||
super(params);
|
*
|
||||||
this.state = {
|
* @param {Number} millis
|
||||||
diffColor: undefined,
|
* @returns The formatted time
|
||||||
};
|
*/
|
||||||
|
function msToMinSeconds(millis) {
|
||||||
|
const minutes = Math.floor(millis / 60000);
|
||||||
|
const seconds = Number(((millis % 60000) / 1000).toFixed(0));
|
||||||
|
return seconds === 60
|
||||||
|
? minutes + 1 + ":00"
|
||||||
|
: minutes + ":" + (seconds < 10 ? "0" : "") + seconds;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the difficulity color from the given difficulity
|
||||||
|
*
|
||||||
|
* @param {string} diff
|
||||||
|
*/
|
||||||
|
function formatDiff(diff) {
|
||||||
|
if (diff === "ExpertPlus") {
|
||||||
|
return "#8f48db";
|
||||||
}
|
}
|
||||||
|
if (diff === "Expert") {
|
||||||
componentDidMount() {
|
return "#bf2a42";
|
||||||
const data = this.props.data.songData.status.beatmap;
|
|
||||||
this.formatDiff(data.difficulty);
|
|
||||||
}
|
}
|
||||||
|
if (diff === "Hard") {
|
||||||
/**
|
return "tomato";
|
||||||
* Update the difficulity color from the given difficulity
|
|
||||||
*
|
|
||||||
* @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" });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
if (diff === "Normal") {
|
||||||
/**
|
return "#59b0f4";
|
||||||
* Format the given ms
|
|
||||||
*
|
|
||||||
* @param {Number} millis
|
|
||||||
* @returns The formatted time
|
|
||||||
*/
|
|
||||||
msToMinSeconds(millis) {
|
|
||||||
const minutes = Math.floor(millis / 60000);
|
|
||||||
const seconds = Number(((millis % 60000) / 1000).toFixed(0));
|
|
||||||
return seconds === 60
|
|
||||||
? minutes + 1 + ":00"
|
|
||||||
: minutes + ":" + (seconds < 10 ? "0" : "") + seconds;
|
|
||||||
}
|
}
|
||||||
|
if (diff === "Easy") {
|
||||||
render() {
|
return "MediumSeaGreen";
|
||||||
const data = this.props.data.songData.status.beatmap;
|
|
||||||
const beatSaverData = this.props.data.beatSaverData.data;
|
|
||||||
const mapArt = beatSaverData.mapArt;
|
|
||||||
const bsr = beatSaverData.bsr;
|
|
||||||
const { songName, songAuthorName, difficulty } = data;
|
|
||||||
const songTimerPercentage =
|
|
||||||
(this.props.data.currentSongTime / data.length) * 100000;
|
|
||||||
|
|
||||||
const cssVars = document.querySelector("." + styles.songInfoContainer);
|
|
||||||
if (cssVars) {
|
|
||||||
if (!this.props.data.isPlayerInfoVisible) {
|
|
||||||
cssVars.style.setProperty("margin-top", "5px");
|
|
||||||
cssVars.style.setProperty("--pos", "none");
|
|
||||||
} else {
|
|
||||||
cssVars.style.setProperty("margin-top", "0px");
|
|
||||||
cssVars.style.setProperty("--pos", 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={styles.songInfoContainer}>
|
|
||||||
<Image
|
|
||||||
width={150}
|
|
||||||
height={150}
|
|
||||||
alt="Song artwork"
|
|
||||||
src={mapArt}
|
|
||||||
loading="lazy"
|
|
||||||
placeholder="blur"
|
|
||||||
blurDataURL="https://cdn.fascinated.cc/IkQFyodbZv.jpg?raw=true"
|
|
||||||
/>
|
|
||||||
<div className={styles.songInfo}>
|
|
||||||
<p className={styles.songInfoSongName}>
|
|
||||||
{songName.length > 35
|
|
||||||
? songName.substring(0, 35) + "..."
|
|
||||||
: songName}
|
|
||||||
</p>
|
|
||||||
<p className={styles.songInfoSongAuthor}>{songAuthorName}</p>
|
|
||||||
<div className={styles.songInfoSongOtherContainer}>
|
|
||||||
<p
|
|
||||||
className={styles.songInfoDiff}
|
|
||||||
style={{ backgroundColor: this.state.diffColor }}
|
|
||||||
>
|
|
||||||
{difficulty}
|
|
||||||
</p>
|
|
||||||
<p className={styles.songInfoBsr}>!bsr {bsr}</p>
|
|
||||||
</div>
|
|
||||||
<p className={styles.songTimeText}>
|
|
||||||
{this.msToMinSeconds(this.props.data.currentSongTime * 1000)}/
|
|
||||||
{this.msToMinSeconds(data.length)}
|
|
||||||
</p>
|
|
||||||
<div className={styles.songTimeContainer}>
|
|
||||||
<div className={styles.songTimeBackground} />
|
|
||||||
<div
|
|
||||||
className={styles.songTime}
|
|
||||||
style={{ width: songTimerPercentage + "%" }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default function SongInfo() {
|
||||||
|
const [showSongInfo, shouldReplacePlayerInfoWithScore] = useSettingsStore(
|
||||||
|
(store) => [store.showSongInfo, store.shouldReplacePlayerInfoWithScore]
|
||||||
|
);
|
||||||
|
const [
|
||||||
|
isLoading,
|
||||||
|
bsr,
|
||||||
|
mapArt,
|
||||||
|
songTitle,
|
||||||
|
songSubTitle,
|
||||||
|
songDifficulty,
|
||||||
|
currentSongTime,
|
||||||
|
songLength,
|
||||||
|
] = useSongDataStore((store) => [
|
||||||
|
store.isLoading,
|
||||||
|
store.bsr,
|
||||||
|
store.mapArt,
|
||||||
|
store.songTitle,
|
||||||
|
store.songSubTitle,
|
||||||
|
store.songDifficulty,
|
||||||
|
store.currentSongTime,
|
||||||
|
store.songLength,
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!showSongInfo) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const songTimerPercentage = (currentSongTime / songLength) * 100000;
|
||||||
|
const diffColor = formatDiff(songDifficulty);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={styles.songInfoContainer}
|
||||||
|
style={{
|
||||||
|
bottom: shouldReplacePlayerInfoWithScore ? "" : 0,
|
||||||
|
left: shouldReplacePlayerInfoWithScore ? "" : 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
width={150}
|
||||||
|
height={150}
|
||||||
|
alt="Song artwork"
|
||||||
|
src={mapArt}
|
||||||
|
loading="lazy"
|
||||||
|
placeholder="blur"
|
||||||
|
blurDataURL="https://cdn.fascinated.cc/IkQFyodbZv.jpg?raw=true"
|
||||||
|
/>
|
||||||
|
<div className={styles.songInfo}>
|
||||||
|
<p className={styles.songInfoSongName}>
|
||||||
|
{songTitle.length > 35
|
||||||
|
? songTitle.substring(0, 35) + "..."
|
||||||
|
: songTitle}
|
||||||
|
</p>
|
||||||
|
<p className={styles.songInfoSongSubName}>{songSubTitle}</p>
|
||||||
|
<div className={styles.songInfoSongOtherContainer}>
|
||||||
|
<p
|
||||||
|
className={styles.songInfoDiff}
|
||||||
|
style={{ backgroundColor: diffColor }}
|
||||||
|
>
|
||||||
|
{songDifficulty.replace("Plus", "+")}
|
||||||
|
</p>
|
||||||
|
<p className={styles.songInfoBsr}>!bsr {bsr}</p>
|
||||||
|
</div>
|
||||||
|
<p className={styles.songTimeText}>
|
||||||
|
{msToMinSeconds(currentSongTime * 1000)}/{msToMinSeconds(songLength)}
|
||||||
|
</p>
|
||||||
|
<div className={styles.songTimeContainer}>
|
||||||
|
<div className={styles.songTimeBackground} />
|
||||||
|
<div
|
||||||
|
className={styles.songTime}
|
||||||
|
style={{ width: songTimerPercentage + "%" }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import env from "@beam-australia/react-env";
|
import env from "@beam-australia/react-env";
|
||||||
import { VARS } from "./EnvVars";
|
import { VARS } from "./EnvVars";
|
||||||
|
|
||||||
const WebsiteTypes = {
|
const LeaderboardType = {
|
||||||
ScoreSaber: {
|
ScoreSaber: {
|
||||||
ApiUrl: {
|
ApiUrl: {
|
||||||
PlayerData:
|
PlayerData:
|
||||||
@ -33,4 +33,4 @@ const WebsiteTypes = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default WebsiteTypes;
|
export default LeaderboardType;
|
||||||
|
3
src/helpers/map/mapHelpers.ts
Normal file
3
src/helpers/map/mapHelpers.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export function getMapHashFromLevelId(levelId: string): string {
|
||||||
|
return levelId.replace("custom_level_", "");
|
||||||
|
}
|
14
src/helpers/web/getPlayerData.js
Normal file
14
src/helpers/web/getPlayerData.js
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import axios from "axios";
|
||||||
|
import LeaderboardType from "../../consts/LeaderboardType";
|
||||||
|
|
||||||
|
export async function getPlayerData(leaderboardType, playerId) {
|
||||||
|
const data = await axios.get(
|
||||||
|
LeaderboardType[leaderboardType].ApiUrl.PlayerData.replace("%s", playerId),
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
"x-requested-with": "BeatSaber Overlay",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return data;
|
||||||
|
}
|
217
src/helpers/websocketClient.ts
Normal file
217
src/helpers/websocketClient.ts
Normal file
@ -0,0 +1,217 @@
|
|||||||
|
import { w3cwebsocket as W3CWebSocket } from "websocket";
|
||||||
|
import { useSettingsStore } from "../store/overlaySettingsStore";
|
||||||
|
import { usePlayerDataStore } from "../store/playerDataStore";
|
||||||
|
import { useSongDataStore } from "../store/songDataStore";
|
||||||
|
import Utils from "../utils/utils";
|
||||||
|
import { getMapHashFromLevelId } from "./map/mapHelpers";
|
||||||
|
|
||||||
|
const ip = useSettingsStore.getState().socketAddr;
|
||||||
|
let hasConnected = false;
|
||||||
|
|
||||||
|
let cutData: any = [];
|
||||||
|
cutData.saberA = {
|
||||||
|
count: [0, 0, 0],
|
||||||
|
totalScore: [0, 0, 0],
|
||||||
|
};
|
||||||
|
cutData.saberB = {
|
||||||
|
count: [0, 0, 0],
|
||||||
|
totalScore: [0, 0, 0],
|
||||||
|
};
|
||||||
|
|
||||||
|
const updatePlayerData = usePlayerDataStore.getState().updatePlayerData;
|
||||||
|
|
||||||
|
export function connectClient() {
|
||||||
|
const client = new W3CWebSocket(`ws://${ip}:6557/socket`);
|
||||||
|
|
||||||
|
if (hasConnected) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
client.onopen = () => {
|
||||||
|
console.log("WebSocket Client Connected");
|
||||||
|
hasConnected = true;
|
||||||
|
};
|
||||||
|
client.onclose = () => {
|
||||||
|
if (hasConnected) {
|
||||||
|
console.log(
|
||||||
|
"Lost connection to HTTPSiraStatus, attempting to reconnect."
|
||||||
|
);
|
||||||
|
connectClient();
|
||||||
|
} else {
|
||||||
|
hasConnected = false;
|
||||||
|
console.log(
|
||||||
|
"Unable to connect to HTTPSiraStatus, retrying in 30 seconds."
|
||||||
|
);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
connectClient();
|
||||||
|
}, 30_000);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
client.onmessage = (message: any) => {
|
||||||
|
const data: string = message.data;
|
||||||
|
const json = JSON.parse(data);
|
||||||
|
if (!handlers[json.event]) {
|
||||||
|
console.log("Unhandled message from HTTP Status. (" + json.event + ")");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
handlers[json.event](json || []);
|
||||||
|
try {
|
||||||
|
const time = json.status.performance.currentSongTime;
|
||||||
|
if (time !== undefined && time != null) {
|
||||||
|
useSongDataStore.setState({ currentSongTime: time });
|
||||||
|
}
|
||||||
|
} catch (e) {}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlers: any = {
|
||||||
|
hello: (data: any) => {
|
||||||
|
console.log("Hello from HttpSiraStatus");
|
||||||
|
|
||||||
|
if (data.status && data.status.beatmap) {
|
||||||
|
console.log("Going into level during song, resetting data.");
|
||||||
|
const state = useSongDataStore.getState();
|
||||||
|
const {
|
||||||
|
levelId,
|
||||||
|
difficultyEnum,
|
||||||
|
characteristic,
|
||||||
|
songName,
|
||||||
|
songSubName,
|
||||||
|
levelAuthorName,
|
||||||
|
length,
|
||||||
|
} = data.status.beatmap;
|
||||||
|
state.reset();
|
||||||
|
state.setInSong(true);
|
||||||
|
state.updateMapData(
|
||||||
|
getMapHashFromLevelId(levelId),
|
||||||
|
difficultyEnum,
|
||||||
|
characteristic,
|
||||||
|
songName,
|
||||||
|
songSubName || levelAuthorName,
|
||||||
|
length
|
||||||
|
);
|
||||||
|
|
||||||
|
const { score, relativeScore } = data.status.performance;
|
||||||
|
let finalScore = score;
|
||||||
|
if (finalScore == 0) {
|
||||||
|
finalScore = state.currentScore;
|
||||||
|
}
|
||||||
|
const percent = relativeScore * 100;
|
||||||
|
|
||||||
|
state.setCurrentScore(finalScore);
|
||||||
|
state.setPercent(percent.toFixed(2) + "%");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
songStart: (data: any) => {
|
||||||
|
console.log("Going into level during song, resetting data.");
|
||||||
|
const state = useSongDataStore.getState();
|
||||||
|
const {
|
||||||
|
levelId,
|
||||||
|
difficultyEnum,
|
||||||
|
characteristic,
|
||||||
|
songName,
|
||||||
|
songSubName,
|
||||||
|
length,
|
||||||
|
} = data.status.beatmap;
|
||||||
|
|
||||||
|
state.reset();
|
||||||
|
state.setInSong(true);
|
||||||
|
state.updateMapData(
|
||||||
|
getMapHashFromLevelId(levelId),
|
||||||
|
difficultyEnum,
|
||||||
|
characteristic,
|
||||||
|
songName,
|
||||||
|
songSubName,
|
||||||
|
length
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
scoreChanged: (data: any) => {
|
||||||
|
const state = useSongDataStore.getState();
|
||||||
|
const { status } = data;
|
||||||
|
const { score, relativeScore } = status.performance;
|
||||||
|
let finalScore = score;
|
||||||
|
if (finalScore == 0) {
|
||||||
|
finalScore = state.currentScore;
|
||||||
|
}
|
||||||
|
const percent = relativeScore * 100;
|
||||||
|
|
||||||
|
state.setCurrentScore(finalScore);
|
||||||
|
state.setPercent(percent.toFixed(2) + "%");
|
||||||
|
|
||||||
|
const leaderboardType = useSettingsStore.getState().leaderboardType;
|
||||||
|
let currentPP = Utils.calculatePP(
|
||||||
|
state.mapStarCount,
|
||||||
|
percent,
|
||||||
|
leaderboardType
|
||||||
|
);
|
||||||
|
if (currentPP === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
state.setPp(currentPP);
|
||||||
|
},
|
||||||
|
|
||||||
|
noteFullyCut: (data: any) => {
|
||||||
|
const { noteCut } = data;
|
||||||
|
const state: any = useSongDataStore.getState();
|
||||||
|
const parts = noteCut.saberType.split("Saber");
|
||||||
|
const saberType = "saber" + parts[1];
|
||||||
|
|
||||||
|
let beforeCutScore = 0.0;
|
||||||
|
let afterCutScore = 0.0;
|
||||||
|
let cutDistanceScore = 0.0;
|
||||||
|
|
||||||
|
const cutDataSaber = cutData[saberType];
|
||||||
|
cutDataSaber.count[0]++;
|
||||||
|
cutDataSaber.count[1]++;
|
||||||
|
cutDataSaber.count[2]++;
|
||||||
|
cutDataSaber.totalScore[0] += noteCut.beforeCutScore;
|
||||||
|
cutDataSaber.totalScore[1] += noteCut.afterCutScore;
|
||||||
|
cutDataSaber.totalScore[2] += noteCut.cutDistanceScore;
|
||||||
|
beforeCutScore = cutDataSaber.totalScore[0] / cutDataSaber.count[0];
|
||||||
|
afterCutScore = cutDataSaber.totalScore[1] / cutDataSaber.count[1];
|
||||||
|
cutDistanceScore = cutDataSaber.totalScore[2] / cutDataSaber.count[2];
|
||||||
|
|
||||||
|
state.setSaberData(saberType, {
|
||||||
|
averagePreSwing: beforeCutScore,
|
||||||
|
averagePostSwing: afterCutScore,
|
||||||
|
cutDistanceScore: cutDistanceScore,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
finished: () => {
|
||||||
|
const state = useSongDataStore.getState();
|
||||||
|
state.reset();
|
||||||
|
state.setInSong(false);
|
||||||
|
updatePlayerData();
|
||||||
|
},
|
||||||
|
menu: () => {
|
||||||
|
const state = useSongDataStore.getState();
|
||||||
|
state.reset();
|
||||||
|
state.setInSong(false);
|
||||||
|
updatePlayerData();
|
||||||
|
},
|
||||||
|
softFail: () => {
|
||||||
|
const state = useSongDataStore.getState();
|
||||||
|
state.setFailed(true);
|
||||||
|
},
|
||||||
|
pause: () => {
|
||||||
|
const state = useSongDataStore.getState();
|
||||||
|
state.setPaused(true);
|
||||||
|
},
|
||||||
|
resume: () => {
|
||||||
|
const state = useSongDataStore.getState();
|
||||||
|
state.setPaused(false);
|
||||||
|
},
|
||||||
|
noteCut: () => {},
|
||||||
|
noteMissed: () => {},
|
||||||
|
noteSpawned: () => {},
|
||||||
|
bombMissed: () => {},
|
||||||
|
beatmapEvent: () => {},
|
||||||
|
energyChanged: () => {},
|
||||||
|
obstacleEnter: () => {},
|
||||||
|
obstacleExit: () => {},
|
||||||
|
};
|
@ -37,7 +37,7 @@ export default class Home extends Component {
|
|||||||
|
|
||||||
values: {
|
values: {
|
||||||
socketAddr: undefined,
|
socketAddr: undefined,
|
||||||
leaderboard: "ScoreSaber",
|
leaderboardType: "ScoreSaber",
|
||||||
showPlayerStats: true,
|
showPlayerStats: true,
|
||||||
showScoreInfo: false,
|
showScoreInfo: false,
|
||||||
showSongInfo: false,
|
showSongInfo: false,
|
||||||
@ -72,7 +72,10 @@ export default class Home extends Component {
|
|||||||
const json = JSON.parse(localStorage.getItem("values"));
|
const json = JSON.parse(localStorage.getItem("values"));
|
||||||
let values = {};
|
let values = {};
|
||||||
Object.entries(json.values).forEach((value) => {
|
Object.entries(json.values).forEach((value) => {
|
||||||
if (value[0] !== undefined) {
|
if (
|
||||||
|
value[0] !== undefined &&
|
||||||
|
this.state.values[value[0]] !== undefined
|
||||||
|
) {
|
||||||
values[value[0]] = value[1];
|
values[value[0]] = value[1];
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -96,10 +99,6 @@ export default class Home extends Component {
|
|||||||
if (value[1] === undefined) {
|
if (value[1] === undefined) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (value[0] == "leaderboard" && value[1] === "BeatLeader") {
|
|
||||||
values += `&beatLeader=true`;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
values += `&${value[0]}=${value[1]}`;
|
values += `&${value[0]}=${value[1]}`;
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -234,9 +233,11 @@ export default class Home extends Component {
|
|||||||
<Spacer y={1} />
|
<Spacer y={1} />
|
||||||
<Text>Ranked leaderboard</Text>
|
<Text>Ranked leaderboard</Text>
|
||||||
<Radio.Group
|
<Radio.Group
|
||||||
defaultValue={this.state.values.leaderboard || "ScoreSaber"}
|
defaultValue={
|
||||||
|
this.state.values.leaderboardType || "ScoreSaber"
|
||||||
|
}
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
this.updateValue("leaderboard", value);
|
this.updateValue("leaderboardType", value);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Radio
|
<Radio
|
||||||
|
@ -1,516 +1,78 @@
|
|||||||
import { Link, Spinner } from "@nextui-org/react";
|
import axios from "axios";
|
||||||
import { NextSeo } from "next-seo";
|
import { useEffect } from "react";
|
||||||
import { Component } from "react";
|
import PlayerStats from "../components/PlayerStats";
|
||||||
import PlayerStats from "../../src/components/PlayerStats";
|
import ScoreStats from "../components/ScoreStats";
|
||||||
import ScoreStats from "../../src/components/ScoreStats";
|
import SongInfo from "../components/SongInfo";
|
||||||
import SongInfo from "../../src/components/SongInfo";
|
import { connectClient } from "../helpers/websocketClient";
|
||||||
import LeaderboardType from "../../src/consts/LeaderboardType";
|
import { useSettingsStore } from "../store/overlaySettingsStore";
|
||||||
import Utils from "../../src/utils/utils";
|
import { usePlayerDataStore } from "../store/playerDataStore";
|
||||||
|
|
||||||
import styles from "../styles/overlay.module.css";
|
import styles from "../styles/overlay.module.css";
|
||||||
|
|
||||||
export default class Overlay extends Component {
|
export default function Overlay(props) {
|
||||||
#_beatSaverURL = "";
|
const query = JSON.parse(props.query);
|
||||||
|
const [setOverlaySettings, mounted, setMounted] = useSettingsStore(
|
||||||
|
(state) => [state.setOverlaySettings, state.mounted, state.setMounted]
|
||||||
|
);
|
||||||
|
const updatePlayerData = usePlayerDataStore(
|
||||||
|
(state) => state.updatePlayerData
|
||||||
|
);
|
||||||
|
|
||||||
constructor(props) {
|
useEffect(() => {
|
||||||
super(props);
|
if (!mounted && props.isValidSteamId) {
|
||||||
|
setMounted(true);
|
||||||
|
|
||||||
this.cutData = [];
|
async function setup() {
|
||||||
this.cutData.SaberA = {
|
await setOverlaySettings(query);
|
||||||
count: [0, 0, 0],
|
const showSongInfo = useSettingsStore.getState().showSongInfo;
|
||||||
totalScore: [0, 0, 0],
|
const showScoreInfo = useSettingsStore.getState().showScoreInfo;
|
||||||
};
|
if (showSongInfo || (showScoreInfo && typeof window !== "undefined")) {
|
||||||
this.cutData.SaberB = {
|
await connectClient();
|
||||||
count: [0, 0, 0],
|
}
|
||||||
totalScore: [0, 0, 0],
|
const showPlayerStats = useSettingsStore.getState().showPlayerStats;
|
||||||
};
|
if (showPlayerStats) {
|
||||||
|
await updatePlayerData();
|
||||||
this._mounted = false;
|
|
||||||
this.state = {
|
|
||||||
hasError: false,
|
|
||||||
|
|
||||||
// Steam ID
|
|
||||||
id: undefined,
|
|
||||||
|
|
||||||
// Values from the query parameters
|
|
||||||
loadingPlayerData: true,
|
|
||||||
isConnectedToSocket: false,
|
|
||||||
isValidSteamId: false,
|
|
||||||
websiteType: "ScoreSaber",
|
|
||||||
showPlayerStats: true,
|
|
||||||
showScore: false,
|
|
||||||
showSongInfo: false,
|
|
||||||
shouldReplacePlayerInfoWithScore: false,
|
|
||||||
|
|
||||||
// Internal
|
|
||||||
loadedDuringSong: false,
|
|
||||||
socket: undefined,
|
|
||||||
data: undefined,
|
|
||||||
beatSaverData: undefined,
|
|
||||||
songInfo: undefined,
|
|
||||||
mapStarCount: undefined,
|
|
||||||
|
|
||||||
// UI elements
|
|
||||||
isPlayerInfoVisible: false,
|
|
||||||
isVisible: false,
|
|
||||||
|
|
||||||
// Score data
|
|
||||||
paused: true,
|
|
||||||
failed: false,
|
|
||||||
currentSongTime: 0,
|
|
||||||
currentScore: 0,
|
|
||||||
percentage: "100.00%",
|
|
||||||
SaberA: {
|
|
||||||
cutDistanceScore: 0.0,
|
|
||||||
averagePreSwing: 0.0,
|
|
||||||
averagePostSwing: 0.0,
|
|
||||||
},
|
|
||||||
SaberB: {
|
|
||||||
cutDistanceScore: 0.0,
|
|
||||||
averagePreSwing: 0.0,
|
|
||||||
averagePostSwing: 0.0,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
this.setupTimer();
|
|
||||||
}
|
|
||||||
|
|
||||||
async componentDidMount() {
|
|
||||||
if (this._mounted === true) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this._mounted = true;
|
|
||||||
|
|
||||||
if (this.state.hasError) {
|
|
||||||
// Reload the page if there has been an error
|
|
||||||
console.log("There has been an error and the page was reloaded.");
|
|
||||||
return Router.reload(window.location.pathname);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("Initializing...");
|
|
||||||
this.#_beatSaverURL =
|
|
||||||
document.location.origin + "/api/beatsaver/map?hash=%s";
|
|
||||||
const urlSearchParams = new URLSearchParams(window.location.search);
|
|
||||||
const params = Object.fromEntries(urlSearchParams.entries());
|
|
||||||
|
|
||||||
// Check what website the player wants to use
|
|
||||||
if (params.beatleader === "true" || params.beatLeader === "true") {
|
|
||||||
this.setState({ websiteType: "BeatLeader" });
|
|
||||||
}
|
|
||||||
|
|
||||||
const id = params.id;
|
|
||||||
if (!id) {
|
|
||||||
// Check if the id param is valid
|
|
||||||
this.setState({ isValidSteamId: false, loadingPlayerData: false });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Checks if the steam id is valid
|
|
||||||
const isValid = await this.validateSteamId(id);
|
|
||||||
if (!isValid) {
|
|
||||||
this.setState({ isValidSteamId: false, loadingPlayerData: false });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.setState({ id: id, isValidSteamId: true });
|
|
||||||
|
|
||||||
// Check if the player wants to disable their stats (pp, global pos, etc)
|
|
||||||
if (params.showPlayerStats === "false" || params.playerstats === "false") {
|
|
||||||
this.setState({ showPlayerStats: false });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.state.showPlayerStats == true || params.playerstats == "true") {
|
|
||||||
setTimeout(async () => {
|
|
||||||
await this.updateData(id);
|
|
||||||
}, 10); // 10ms
|
|
||||||
this.setState({ isPlayerInfoVisible: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
let shouldConnectSocket = false;
|
|
||||||
|
|
||||||
// Check if the player wants to show their current score information
|
|
||||||
if (params.showScoreInfo === "true" || params.scoreinfo === "true") {
|
|
||||||
this.setState({ showScore: true });
|
|
||||||
shouldConnectSocket = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (params.shouldReplacePlayerInfoWithScore === "true") {
|
|
||||||
this.setState({ shouldReplacePlayerInfoWithScore: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if the player wants to show the current song
|
|
||||||
if (params.showSongInfo === "true" || params.songinfo === "true") {
|
|
||||||
this.setState({ showSongInfo: true });
|
|
||||||
shouldConnectSocket = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (shouldConnectSocket) {
|
|
||||||
if (this.state.isConnectedToSocket) return;
|
|
||||||
this.connectSocket(params.socketaddress);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle Errors
|
|
||||||
static getDerivedStateFromError(error) {
|
|
||||||
return this.setState({ hasError: true });
|
|
||||||
}
|
|
||||||
componentDidCatch(error, errorInfo) {
|
|
||||||
console.log({ error, errorInfo });
|
|
||||||
}
|
|
||||||
// ---
|
|
||||||
|
|
||||||
// I'd love if HTTP Status just gave this data lmao
|
|
||||||
// HttpSiraStatus(https://github.com/denpadokei/HttpSiraStatus) does give this data.
|
|
||||||
isCurrentSongTimeProvided = false;
|
|
||||||
// we don't need to reset this to false because it is highly unlikely for a player to swap mods within a browser session
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Setup the timer for the song time
|
|
||||||
*/
|
|
||||||
setupTimer() {
|
|
||||||
setInterval(() => {
|
|
||||||
if (this.isCurrentSongTimeProvided) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!this.state.paused && this.state.beatSaverData !== undefined) {
|
|
||||||
this.setState({ currentSongTime: this.state.currentSongTime + 1 });
|
|
||||||
}
|
|
||||||
}, 1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update the current song time
|
|
||||||
*
|
|
||||||
* @param {[]} data The song data
|
|
||||||
*/
|
|
||||||
handleCurrentSongTime(data) {
|
|
||||||
try {
|
|
||||||
const time = data.status.performance.currentSongTime;
|
|
||||||
if (time !== undefined && time != null) {
|
|
||||||
this.isCurrentSongTimeProvided = true;
|
|
||||||
this.setState({ currentSongTime: time });
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
// do nothing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetch and update the data from the respective platform
|
|
||||||
*
|
|
||||||
* @param {string} id The steam id of the player
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
async updateData(id) {
|
|
||||||
const data = await fetch(
|
|
||||||
Utils.getWebsiteApi(this.state.websiteType).ApiUrl.PlayerData.replace(
|
|
||||||
"%s",
|
|
||||||
id
|
|
||||||
),
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
"X-Requested-With": "BeatSaber Overlay",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
try {
|
|
||||||
const json = await data.json();
|
|
||||||
this.setState({
|
|
||||||
loadingPlayerData: false,
|
|
||||||
id: id,
|
|
||||||
data: json,
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
// Catch error and use last known data
|
|
||||||
console.error(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if the given steam id is valid or not
|
|
||||||
*
|
|
||||||
* @param {id} The Steam ID of the player to validate
|
|
||||||
*/
|
|
||||||
async validateSteamId(id) {
|
|
||||||
if (id.length !== 17) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const data = await fetch(`/api/validateid?steamid=${id}`);
|
|
||||||
const json = await data.json();
|
|
||||||
return json.message === "Valid";
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Setup the HTTP Status connection
|
|
||||||
*/
|
|
||||||
connectSocket(socketAddress) {
|
|
||||||
socketAddress =
|
|
||||||
(socketAddress === undefined
|
|
||||||
? "ws://localhost"
|
|
||||||
: `ws://${socketAddress}`) + ":6557/socket";
|
|
||||||
if (this.state.isConnectedToSocket) return;
|
|
||||||
|
|
||||||
if (this.state.isVisible) {
|
|
||||||
this.resetData(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`Connecting to ${socketAddress}`);
|
|
||||||
const socket = new WebSocket(socketAddress);
|
|
||||||
socket.addEventListener("open", () => {
|
|
||||||
console.log(`Connected to ${socketAddress}`);
|
|
||||||
this.setState({ isConnectedToSocket: true });
|
|
||||||
});
|
|
||||||
socket.addEventListener("close", () => {
|
|
||||||
console.log(
|
|
||||||
"Attempting to re-connect to the HTTP Status socket in 60 seconds."
|
|
||||||
);
|
|
||||||
this.resetData(false);
|
|
||||||
this.setState({ isConnectedToSocket: false });
|
|
||||||
setTimeout(() => this.connectSocket(), 60_000);
|
|
||||||
});
|
|
||||||
socket.addEventListener("message", (message) => {
|
|
||||||
const json = JSON.parse(message.data);
|
|
||||||
this.handleCurrentSongTime(json);
|
|
||||||
if (!this.handlers[json.event]) {
|
|
||||||
console.log("Unhandled message from HTTP Status. (" + json.event + ")");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.handlers[json.event](json || []);
|
|
||||||
});
|
|
||||||
this.setState({ socket: socket });
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the current songs beat saver url in {@link #_beatSaverURL}
|
|
||||||
*
|
|
||||||
* @param {[]} songData
|
|
||||||
*/
|
|
||||||
async setBeatSaver(songData) {
|
|
||||||
console.log("Updating BeatSaver info");
|
|
||||||
const data = await fetch(
|
|
||||||
this.#_beatSaverURL.replace("%s", songData.levelId)
|
|
||||||
);
|
|
||||||
const json = await data.json();
|
|
||||||
this.setState({ beatSaverData: json });
|
|
||||||
|
|
||||||
const { characteristic, levelId, difficulty } = songData;
|
|
||||||
let mapHash = levelId.replace("custom_level_", "");
|
|
||||||
let mapStars = undefined;
|
|
||||||
if (this.state.websiteType === "BeatLeader") {
|
|
||||||
mapStars = await LeaderboardType.BeatLeader.getMapStarCount(
|
|
||||||
mapHash,
|
|
||||||
difficulty.replace("+", "Plus"),
|
|
||||||
characteristic
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (this.state.websiteType === "ScoreSaber") {
|
|
||||||
mapStars = await LeaderboardType.ScoreSaber.getMapStarCount(
|
|
||||||
mapHash,
|
|
||||||
difficulty.replace("+", "Plus"),
|
|
||||||
characteristic
|
|
||||||
);
|
|
||||||
}
|
|
||||||
this.setState({ mapStarCount: mapStars });
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Cleanup the data and get ready for the next song
|
|
||||||
*
|
|
||||||
* @param {boolean} visible Whether to show info other than the player stats
|
|
||||||
*/
|
|
||||||
async resetData(visible, loadedDuringSong = false) {
|
|
||||||
if (this.state.showPlayerStats == true) {
|
|
||||||
setTimeout(async () => {
|
|
||||||
await this.updateData(this.state.id);
|
|
||||||
}, 1000); // 1 second
|
|
||||||
}
|
|
||||||
|
|
||||||
if (visible && this.state.shouldReplacePlayerInfoWithScore) {
|
|
||||||
this.setState({ isPlayerInfoVisible: false });
|
|
||||||
} else {
|
|
||||||
this.setState({ isPlayerInfoVisible: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
this.cutData = [];
|
|
||||||
this.cutData.SaberA = {
|
|
||||||
count: [0, 0, 0],
|
|
||||||
totalScore: [0, 0, 0],
|
|
||||||
};
|
|
||||||
this.cutData.SaberB = {
|
|
||||||
count: [0, 0, 0],
|
|
||||||
totalScore: [0, 0, 0],
|
|
||||||
};
|
|
||||||
this.setState({
|
|
||||||
SaberA: {
|
|
||||||
cutDistanceScore: 0.0,
|
|
||||||
averagePreSwing: 0.0,
|
|
||||||
averagePostSwing: 0.0,
|
|
||||||
},
|
|
||||||
SaberB: {
|
|
||||||
cutDistanceScore: 0.0,
|
|
||||||
averagePreSwing: 0.0,
|
|
||||||
averagePostSwing: 0.0,
|
|
||||||
},
|
|
||||||
songInfo: undefined,
|
|
||||||
beatSaverData: undefined,
|
|
||||||
currentSongTime: 0,
|
|
||||||
currentScore: 0,
|
|
||||||
percentage: "100.00%",
|
|
||||||
isVisible: visible,
|
|
||||||
loadedDuringSong: loadedDuringSong,
|
|
||||||
mapStarCount: undefined,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// The HTTP Status handlers
|
|
||||||
handlers = {
|
|
||||||
hello: (data) => {
|
|
||||||
console.log("Hello from HTTP Status!");
|
|
||||||
if (
|
|
||||||
data.status &&
|
|
||||||
data.status.beatmap &&
|
|
||||||
this.state.songData === undefined
|
|
||||||
) {
|
|
||||||
console.log("Going into level during song, resetting data.");
|
|
||||||
this.resetData(true, true);
|
|
||||||
this.setState({ songData: data, paused: false });
|
|
||||||
if (this.state.showScore || this.state.showSongInfo) {
|
|
||||||
this.setBeatSaver(data.status.beatmap);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
setup();
|
||||||
scoreChanged: (data) => {
|
|
||||||
const { status } = data;
|
|
||||||
const { score, relativeScore } = status.performance;
|
|
||||||
let finalScore = score;
|
|
||||||
if (finalScore == 0) {
|
|
||||||
finalScore = this.state.currentScore;
|
|
||||||
}
|
|
||||||
const percent = relativeScore * 100;
|
|
||||||
this.setState({
|
|
||||||
currentScore: finalScore,
|
|
||||||
percentage: percent.toFixed(2) + "%",
|
|
||||||
});
|
|
||||||
},
|
|
||||||
noteFullyCut: (data) => {
|
|
||||||
const { noteCut } = data;
|
|
||||||
|
|
||||||
let beforeCutScore = 0.0;
|
|
||||||
let afterCutScore = 0.0;
|
|
||||||
let cutDistanceScore = 0.0;
|
|
||||||
|
|
||||||
const cutDataSaber = this.cutData[noteCut.saberType];
|
|
||||||
cutDataSaber.count[0]++;
|
|
||||||
cutDataSaber.count[1]++;
|
|
||||||
cutDataSaber.count[2]++;
|
|
||||||
cutDataSaber.totalScore[0] += noteCut.beforeCutScore;
|
|
||||||
cutDataSaber.totalScore[1] += noteCut.afterCutScore;
|
|
||||||
cutDataSaber.totalScore[2] += noteCut.cutDistanceScore;
|
|
||||||
beforeCutScore = cutDataSaber.totalScore[0] / cutDataSaber.count[0];
|
|
||||||
afterCutScore = cutDataSaber.totalScore[1] / cutDataSaber.count[1];
|
|
||||||
cutDistanceScore = cutDataSaber.totalScore[2] / cutDataSaber.count[2];
|
|
||||||
|
|
||||||
const cutData = this.state[noteCut.saberType];
|
|
||||||
cutData.averagePreSwing = beforeCutScore;
|
|
||||||
cutData.averagePostSwing = afterCutScore;
|
|
||||||
cutData.cutDistanceScore = cutDistanceScore;
|
|
||||||
this.setState({ [noteCut.saberType]: cutData });
|
|
||||||
},
|
|
||||||
songStart: (data) => {
|
|
||||||
console.log("Going into level, resetting data.");
|
|
||||||
this.resetData(true);
|
|
||||||
this.setState({ songData: data, paused: false });
|
|
||||||
if (this.state.showScore || this.state.showSongInfo) {
|
|
||||||
this.setBeatSaver(data.status.beatmap);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
finished: () => {
|
|
||||||
this.resetData(false);
|
|
||||||
},
|
|
||||||
softFail: () => {
|
|
||||||
this.setState({ failed: true });
|
|
||||||
},
|
|
||||||
pause: () => {
|
|
||||||
this.setState({ paused: true });
|
|
||||||
},
|
|
||||||
resume: () => {
|
|
||||||
this.setState({ paused: false });
|
|
||||||
},
|
|
||||||
menu: () => {
|
|
||||||
this.resetData(false);
|
|
||||||
},
|
|
||||||
noteCut: () => {},
|
|
||||||
noteMissed: () => {},
|
|
||||||
noteSpawned: () => {},
|
|
||||||
bombMissed: () => {},
|
|
||||||
beatmapEvent: () => {},
|
|
||||||
energyChanged: () => {},
|
|
||||||
obstacleEnter: () => {},
|
|
||||||
obstacleExit: () => {},
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
isValidSteamId,
|
|
||||||
data,
|
|
||||||
websiteType,
|
|
||||||
showPlayerStats,
|
|
||||||
loadingPlayerData,
|
|
||||||
isPlayerInfoVisible,
|
|
||||||
shouldReplacePlayerInfoWithScore,
|
|
||||||
id,
|
|
||||||
} = this.state;
|
|
||||||
|
|
||||||
if (loadingPlayerData) {
|
|
||||||
return <Spinner size="xl" color="white"></Spinner>;
|
|
||||||
}
|
}
|
||||||
|
}, [
|
||||||
|
query,
|
||||||
|
props.isValidSteamId,
|
||||||
|
setOverlaySettings,
|
||||||
|
mounted,
|
||||||
|
setMounted,
|
||||||
|
updatePlayerData,
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!props.isValidSteamId) {
|
||||||
return (
|
return (
|
||||||
<>
|
<div className={styles.invalidPlayer}>
|
||||||
<NextSeo title="Overlay"></NextSeo>
|
<h1>Invalid Steam ID</h1>
|
||||||
<div className={styles.main}>
|
<h3>Please check the id field in the url</h3>
|
||||||
{!isValidSteamId ? (
|
</div>
|
||||||
<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}>
|
|
||||||
{showPlayerStats &&
|
|
||||||
!loadingPlayerData &&
|
|
||||||
isPlayerInfoVisible &&
|
|
||||||
!shouldReplacePlayerInfoWithScore ? (
|
|
||||||
<PlayerStats
|
|
||||||
pp={data.pp.toLocaleString("en-US", {
|
|
||||||
maximumFractionDigits: 2,
|
|
||||||
minimumFractionDigits: 2,
|
|
||||||
})}
|
|
||||||
globalPos={data.rank.toLocaleString()}
|
|
||||||
country={data.country}
|
|
||||||
countryRank={data.countryRank.toLocaleString()}
|
|
||||||
websiteType={websiteType}
|
|
||||||
avatar={`https://cdn.scoresaber.com/avatars/${id}.jpg`}
|
|
||||||
loadedDuringSong={this.state.loadedDuringSong}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<></>
|
|
||||||
)}
|
|
||||||
{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>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.main}>
|
||||||
|
<PlayerStats />
|
||||||
|
<ScoreStats />
|
||||||
|
<SongInfo />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getServerSideProps(context) {
|
||||||
|
const steamId = context.query.id;
|
||||||
|
const steamIdResponse = await axios.get(
|
||||||
|
`${process.env.REACT_APP_SITE_URL}/api/validateid?steamid=${steamId}`
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
isValidSteamId: steamIdResponse.data.message === "Valid",
|
||||||
|
query: JSON.stringify(context.query),
|
||||||
|
},
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
517
src/pages/overlayold.js
Normal file
517
src/pages/overlayold.js
Normal file
@ -0,0 +1,517 @@
|
|||||||
|
import { Link, Spinner } from "@nextui-org/react";
|
||||||
|
import { NextSeo } from "next-seo";
|
||||||
|
import { Component } from "react";
|
||||||
|
import PlayerStats from "../components/PlayerStats";
|
||||||
|
import ScoreStats from "../components/ScoreStats";
|
||||||
|
import SongInfo from "../components/SongInfo";
|
||||||
|
import LeaderboardType from "../consts/LeaderboardType";
|
||||||
|
import Utils from "../utils/utils";
|
||||||
|
|
||||||
|
import styles from "../styles/overlay.module.css";
|
||||||
|
|
||||||
|
export default class Overlay extends Component {
|
||||||
|
#_beatSaverURL = "";
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.cutData = [];
|
||||||
|
this.cutData.SaberA = {
|
||||||
|
count: [0, 0, 0],
|
||||||
|
totalScore: [0, 0, 0],
|
||||||
|
};
|
||||||
|
this.cutData.SaberB = {
|
||||||
|
count: [0, 0, 0],
|
||||||
|
totalScore: [0, 0, 0],
|
||||||
|
};
|
||||||
|
|
||||||
|
this._mounted = false;
|
||||||
|
this.state = {
|
||||||
|
hasError: false,
|
||||||
|
|
||||||
|
// Steam ID
|
||||||
|
id: undefined,
|
||||||
|
|
||||||
|
// Values from the query parameters
|
||||||
|
loadingPlayerData: true,
|
||||||
|
isConnectedToSocket: false,
|
||||||
|
isValidSteamId: false,
|
||||||
|
websiteType: "ScoreSaber",
|
||||||
|
showPlayerStats: true,
|
||||||
|
showScore: false,
|
||||||
|
showSongInfo: false,
|
||||||
|
shouldReplacePlayerInfoWithScore: false,
|
||||||
|
|
||||||
|
// Internal
|
||||||
|
loadedDuringSong: false,
|
||||||
|
socket: undefined,
|
||||||
|
data: undefined,
|
||||||
|
beatSaverData: undefined,
|
||||||
|
songInfo: undefined,
|
||||||
|
mapStarCount: undefined,
|
||||||
|
|
||||||
|
// UI elements
|
||||||
|
isPlayerInfoVisible: false,
|
||||||
|
isVisible: false,
|
||||||
|
|
||||||
|
// Score data
|
||||||
|
paused: true,
|
||||||
|
failed: false,
|
||||||
|
currentSongTime: 0,
|
||||||
|
currentScore: 0,
|
||||||
|
percentage: "100.00%",
|
||||||
|
SaberA: {
|
||||||
|
cutDistanceScore: 0.0,
|
||||||
|
averagePreSwing: 0.0,
|
||||||
|
averagePostSwing: 0.0,
|
||||||
|
},
|
||||||
|
SaberB: {
|
||||||
|
cutDistanceScore: 0.0,
|
||||||
|
averagePreSwing: 0.0,
|
||||||
|
averagePostSwing: 0.0,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
this.setupTimer();
|
||||||
|
}
|
||||||
|
|
||||||
|
async componentDidMount() {
|
||||||
|
if (this._mounted === true) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._mounted = true;
|
||||||
|
|
||||||
|
if (this.state.hasError) {
|
||||||
|
// Reload the page if there has been an error
|
||||||
|
console.log("There has been an error and the page was reloaded.");
|
||||||
|
return Router.reload(window.location.pathname);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Initializing...");
|
||||||
|
this.#_beatSaverURL =
|
||||||
|
document.location.origin + "/api/beatsaver/map?hash=%s";
|
||||||
|
const urlSearchParams = new URLSearchParams(window.location.search);
|
||||||
|
const params = Object.fromEntries(urlSearchParams.entries());
|
||||||
|
|
||||||
|
// Check what website the player wants to use
|
||||||
|
if (params.beatleader === "true" || params.beatLeader === "true") {
|
||||||
|
this.setState({ websiteType: "BeatLeader" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const id = params.id;
|
||||||
|
if (!id) {
|
||||||
|
// Check if the id param is valid
|
||||||
|
this.setState({ isValidSteamId: false, loadingPlayerData: false });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Checks if the steam id is valid
|
||||||
|
const isValid = await this.validateSteamId(id);
|
||||||
|
if (!isValid) {
|
||||||
|
this.setState({ isValidSteamId: false, loadingPlayerData: false });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.setState({ id: id, isValidSteamId: true });
|
||||||
|
|
||||||
|
// Check if the player wants to disable their stats (pp, global pos, etc)
|
||||||
|
if (params.showPlayerStats === "false" || params.playerstats === "false") {
|
||||||
|
this.setState({ showPlayerStats: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.state.showPlayerStats == true || params.playerstats == "true") {
|
||||||
|
setTimeout(async () => {
|
||||||
|
await this.updateData(id);
|
||||||
|
}, 10); // 10ms
|
||||||
|
this.setState({ isPlayerInfoVisible: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
let shouldConnectSocket = false;
|
||||||
|
|
||||||
|
// Check if the player wants to show their current score information
|
||||||
|
if (params.showScoreInfo === "true" || params.scoreinfo === "true") {
|
||||||
|
this.setState({ showScore: true });
|
||||||
|
shouldConnectSocket = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params.shouldReplacePlayerInfoWithScore === "true") {
|
||||||
|
this.setState({ shouldReplacePlayerInfoWithScore: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the player wants to show the current song
|
||||||
|
if (params.showSongInfo === "true" || params.songinfo === "true") {
|
||||||
|
this.setState({ showSongInfo: true });
|
||||||
|
shouldConnectSocket = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldConnectSocket) {
|
||||||
|
if (this.state.isConnectedToSocket) return;
|
||||||
|
this.connectSocket(params.socketaddress);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle Errors
|
||||||
|
static getDerivedStateFromError(error) {
|
||||||
|
return this.setState({ hasError: true });
|
||||||
|
}
|
||||||
|
componentDidCatch(error, errorInfo) {
|
||||||
|
console.log({ error, errorInfo });
|
||||||
|
}
|
||||||
|
// ---
|
||||||
|
|
||||||
|
// I'd love if HTTP Status just gave this data lmao
|
||||||
|
// HttpSiraStatus(https://github.com/denpadokei/HttpSiraStatus) does give this data.
|
||||||
|
isCurrentSongTimeProvided = false;
|
||||||
|
// we don't need to reset this to false because it is highly unlikely for a player to swap mods within a browser session
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setup the timer for the song time
|
||||||
|
*/
|
||||||
|
setupTimer() {
|
||||||
|
setInterval(() => {
|
||||||
|
if (this.isCurrentSongTimeProvided) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!this.state.paused && this.state.beatSaverData !== undefined) {
|
||||||
|
this.setState({ currentSongTime: this.state.currentSongTime + 1 });
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the current song time
|
||||||
|
*
|
||||||
|
* @param {[]} data The song data
|
||||||
|
*/
|
||||||
|
handleCurrentSongTime(data) {
|
||||||
|
try {
|
||||||
|
const time = data.status.performance.currentSongTime;
|
||||||
|
if (time !== undefined && time != null) {
|
||||||
|
this.isCurrentSongTimeProvided = true;
|
||||||
|
this.setState({ currentSongTime: time });
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// do nothing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch and update the data from the respective platform
|
||||||
|
*
|
||||||
|
* @param {string} id The steam id of the player
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
async updateData(id) {
|
||||||
|
const data = await fetch(
|
||||||
|
Utils.getWebsiteApi(this.state.websiteType).ApiUrl.PlayerData.replace(
|
||||||
|
"%s",
|
||||||
|
id
|
||||||
|
),
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
"X-Requested-With": "BeatSaber Overlay",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
const json = await data.json();
|
||||||
|
this.setState({
|
||||||
|
loadingPlayerData: false,
|
||||||
|
id: id,
|
||||||
|
data: json,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
// Catch error and use last known data
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the given steam id is valid or not
|
||||||
|
*
|
||||||
|
* @param {id} The Steam ID of the player to validate
|
||||||
|
*/
|
||||||
|
async validateSteamId(id) {
|
||||||
|
if (id.length !== 17) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const data = await fetch(`/api/validateid?steamid=${id}`);
|
||||||
|
const json = await data.json();
|
||||||
|
return json.message === "Valid";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setup the HTTP Status connection
|
||||||
|
*/
|
||||||
|
connectSocket(socketAddress) {
|
||||||
|
socketAddress =
|
||||||
|
(socketAddress === undefined
|
||||||
|
? "ws://localhost"
|
||||||
|
: `ws://${socketAddress}`) + ":6557/socket";
|
||||||
|
if (this.state.isConnectedToSocket) return;
|
||||||
|
|
||||||
|
if (this.state.isVisible) {
|
||||||
|
this.resetData(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Connecting to ${socketAddress}`);
|
||||||
|
const socket = new WebSocket(socketAddress);
|
||||||
|
socket.addEventListener("open", () => {
|
||||||
|
console.log(`Connected to ${socketAddress}`);
|
||||||
|
this.setState({ isConnectedToSocket: true });
|
||||||
|
});
|
||||||
|
socket.addEventListener("close", () => {
|
||||||
|
console.log(
|
||||||
|
"Attempting to re-connect to the HTTP Status socket in 60 seconds."
|
||||||
|
);
|
||||||
|
this.resetData(false);
|
||||||
|
this.setState({ isConnectedToSocket: false });
|
||||||
|
setTimeout(() => this.connectSocket(), 60_000);
|
||||||
|
});
|
||||||
|
socket.addEventListener("message", (message) => {
|
||||||
|
const json = JSON.parse(message.data);
|
||||||
|
this.handleCurrentSongTime(json);
|
||||||
|
if (!this.handlers[json.event]) {
|
||||||
|
console.log("Unhandled message from HTTP Status. (" + json.event + ")");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.handlers[json.event](json || []);
|
||||||
|
});
|
||||||
|
this.setState({ socket: socket });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the current songs beat saver url in {@link #_beatSaverURL}
|
||||||
|
*
|
||||||
|
* @param {[]} songData
|
||||||
|
*/
|
||||||
|
async setBeatSaver(songData) {
|
||||||
|
console.log("Updating BeatSaver info");
|
||||||
|
const data = await fetch(
|
||||||
|
this.#_beatSaverURL.replace("%s", songData.levelId)
|
||||||
|
);
|
||||||
|
const json = await data.json();
|
||||||
|
this.setState({ beatSaverData: json });
|
||||||
|
|
||||||
|
const { characteristic, levelId, difficulty } = songData;
|
||||||
|
let mapHash = levelId.replace("custom_level_", "");
|
||||||
|
let mapStars = undefined;
|
||||||
|
if (this.state.websiteType === "BeatLeader") {
|
||||||
|
mapStars = await LeaderboardType.BeatLeader.getMapStarCount(
|
||||||
|
mapHash,
|
||||||
|
difficulty.replace("+", "Plus"),
|
||||||
|
characteristic
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (this.state.websiteType === "ScoreSaber") {
|
||||||
|
mapStars = await LeaderboardType.ScoreSaber.getMapStarCount(
|
||||||
|
mapHash,
|
||||||
|
difficulty.replace("+", "Plus"),
|
||||||
|
characteristic
|
||||||
|
);
|
||||||
|
}
|
||||||
|
this.setState({ mapStarCount: mapStars });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cleanup the data and get ready for the next song
|
||||||
|
*
|
||||||
|
* @param {boolean} visible Whether to show info other than the player stats
|
||||||
|
*/
|
||||||
|
async resetData(visible, loadedDuringSong = false) {
|
||||||
|
if (this.state.showPlayerStats == true) {
|
||||||
|
setTimeout(async () => {
|
||||||
|
await this.updateData(this.state.id);
|
||||||
|
}, 1000); // 1 second
|
||||||
|
}
|
||||||
|
|
||||||
|
if (visible && this.state.shouldReplacePlayerInfoWithScore) {
|
||||||
|
this.setState({ isPlayerInfoVisible: false });
|
||||||
|
} else {
|
||||||
|
this.setState({ isPlayerInfoVisible: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
this.cutData = [];
|
||||||
|
this.cutData.SaberA = {
|
||||||
|
count: [0, 0, 0],
|
||||||
|
totalScore: [0, 0, 0],
|
||||||
|
};
|
||||||
|
this.cutData.SaberB = {
|
||||||
|
count: [0, 0, 0],
|
||||||
|
totalScore: [0, 0, 0],
|
||||||
|
};
|
||||||
|
this.setState({
|
||||||
|
SaberA: {
|
||||||
|
cutDistanceScore: 0.0,
|
||||||
|
averagePreSwing: 0.0,
|
||||||
|
averagePostSwing: 0.0,
|
||||||
|
},
|
||||||
|
SaberB: {
|
||||||
|
cutDistanceScore: 0.0,
|
||||||
|
averagePreSwing: 0.0,
|
||||||
|
averagePostSwing: 0.0,
|
||||||
|
},
|
||||||
|
songInfo: undefined,
|
||||||
|
beatSaverData: undefined,
|
||||||
|
currentSongTime: 0,
|
||||||
|
currentScore: 0,
|
||||||
|
percentage: "100.00%",
|
||||||
|
isVisible: visible,
|
||||||
|
loadedDuringSong: loadedDuringSong,
|
||||||
|
mapStarCount: undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// The HTTP Status handlers
|
||||||
|
handlers = {
|
||||||
|
hello: (data) => {
|
||||||
|
console.log("Hello from HTTP Status!");
|
||||||
|
if (
|
||||||
|
data.status &&
|
||||||
|
data.status.beatmap &&
|
||||||
|
this.state.songData === undefined
|
||||||
|
) {
|
||||||
|
console.log("Going into level during song, resetting data.");
|
||||||
|
this.resetData(true, true);
|
||||||
|
this.setState({ songData: data, paused: false });
|
||||||
|
if (this.state.showScore || this.state.showSongInfo) {
|
||||||
|
this.setBeatSaver(data.status.beatmap);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
scoreChanged: (data) => {
|
||||||
|
const { status } = data;
|
||||||
|
const { score, relativeScore } = status.performance;
|
||||||
|
let finalScore = score;
|
||||||
|
if (finalScore == 0) {
|
||||||
|
finalScore = this.state.currentScore;
|
||||||
|
}
|
||||||
|
const percent = relativeScore * 100;
|
||||||
|
this.setState({
|
||||||
|
currentScore: finalScore,
|
||||||
|
percentage: percent.toFixed(2) + "%",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
noteFullyCut: (data) => {
|
||||||
|
const { noteCut } = data;
|
||||||
|
|
||||||
|
let beforeCutScore = 0.0;
|
||||||
|
let afterCutScore = 0.0;
|
||||||
|
let cutDistanceScore = 0.0;
|
||||||
|
|
||||||
|
const cutDataSaber = this.cutData[noteCut.saberType];
|
||||||
|
cutDataSaber.count[0]++;
|
||||||
|
cutDataSaber.count[1]++;
|
||||||
|
cutDataSaber.count[2]++;
|
||||||
|
cutDataSaber.totalScore[0] += noteCut.beforeCutScore;
|
||||||
|
cutDataSaber.totalScore[1] += noteCut.afterCutScore;
|
||||||
|
cutDataSaber.totalScore[2] += noteCut.cutDistanceScore;
|
||||||
|
beforeCutScore = cutDataSaber.totalScore[0] / cutDataSaber.count[0];
|
||||||
|
afterCutScore = cutDataSaber.totalScore[1] / cutDataSaber.count[1];
|
||||||
|
cutDistanceScore = cutDataSaber.totalScore[2] / cutDataSaber.count[2];
|
||||||
|
|
||||||
|
const cutData = this.state[noteCut.saberType];
|
||||||
|
cutData.averagePreSwing = beforeCutScore;
|
||||||
|
cutData.averagePostSwing = afterCutScore;
|
||||||
|
cutData.cutDistanceScore = cutDistanceScore;
|
||||||
|
this.setState({ [noteCut.saberType]: cutData });
|
||||||
|
},
|
||||||
|
songStart: (data) => {
|
||||||
|
console.log("Going into level, resetting data.");
|
||||||
|
this.resetData(true);
|
||||||
|
this.setState({ songData: data, paused: false });
|
||||||
|
if (this.state.showScore || this.state.showSongInfo) {
|
||||||
|
this.setBeatSaver(data.status.beatmap);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
finished: () => {
|
||||||
|
this.resetData(false);
|
||||||
|
},
|
||||||
|
softFail: () => {
|
||||||
|
this.setState({ failed: true });
|
||||||
|
},
|
||||||
|
pause: () => {
|
||||||
|
this.setState({ paused: true });
|
||||||
|
},
|
||||||
|
resume: () => {
|
||||||
|
this.setState({ paused: false });
|
||||||
|
},
|
||||||
|
menu: () => {
|
||||||
|
this.resetData(false);
|
||||||
|
},
|
||||||
|
noteCut: () => {},
|
||||||
|
noteMissed: () => {},
|
||||||
|
noteSpawned: () => {},
|
||||||
|
bombMissed: () => {},
|
||||||
|
beatmapEvent: () => {},
|
||||||
|
energyChanged: () => {},
|
||||||
|
obstacleEnter: () => {},
|
||||||
|
obstacleExit: () => {},
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
isValidSteamId,
|
||||||
|
data,
|
||||||
|
websiteType,
|
||||||
|
showPlayerStats,
|
||||||
|
loadingPlayerData,
|
||||||
|
isPlayerInfoVisible,
|
||||||
|
shouldReplacePlayerInfoWithScore,
|
||||||
|
id,
|
||||||
|
} = this.state;
|
||||||
|
|
||||||
|
if (loadingPlayerData) {
|
||||||
|
return <Spinner size="xl" color="white"></Spinner>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<NextSeo title="Overlay"></NextSeo>
|
||||||
|
<div className={styles.main}>
|
||||||
|
{!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}>
|
||||||
|
{showPlayerStats && !loadingPlayerData && isPlayerInfoVisible ? (
|
||||||
|
shouldReplacePlayerInfoWithScore ? (
|
||||||
|
<></>
|
||||||
|
) : (
|
||||||
|
<PlayerStats
|
||||||
|
pp={data.pp.toLocaleString("en-US", {
|
||||||
|
maximumFractionDigits: 2,
|
||||||
|
minimumFractionDigits: 2,
|
||||||
|
})}
|
||||||
|
globalPos={data.rank.toLocaleString()}
|
||||||
|
country={data.country}
|
||||||
|
countryRank={data.countryRank.toLocaleString()}
|
||||||
|
websiteType={websiteType}
|
||||||
|
avatar={`https://cdn.scoresaber.com/avatars/${id}.jpg`}
|
||||||
|
loadedDuringSong={this.state.loadedDuringSong}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
<></>
|
||||||
|
)}
|
||||||
|
{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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
18
src/store/overlayDataStore.ts
Normal file
18
src/store/overlayDataStore.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import create from "zustand";
|
||||||
|
|
||||||
|
interface DataState {
|
||||||
|
mounted: boolean;
|
||||||
|
loadedDuringSong: boolean;
|
||||||
|
|
||||||
|
setMounted: (isMounted: boolean) => void;
|
||||||
|
setLoadedDuringSong: (loadedDuringSong: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useDataStore = create<DataState>()((set) => ({
|
||||||
|
mounted: false,
|
||||||
|
loadedDuringSong: false,
|
||||||
|
|
||||||
|
setMounted: (isMounted: boolean) => set(() => ({ mounted: isMounted })),
|
||||||
|
setLoadedDuringSong: (loadedDuringSong: boolean) =>
|
||||||
|
set(() => ({ loadedDuringSong: loadedDuringSong })),
|
||||||
|
}));
|
42
src/store/overlaySettingsStore.ts
Normal file
42
src/store/overlaySettingsStore.ts
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import create from "zustand";
|
||||||
|
import Utils from "../utils/utils";
|
||||||
|
|
||||||
|
interface SettingsState {
|
||||||
|
mounted: boolean;
|
||||||
|
id: string;
|
||||||
|
socketAddr: string;
|
||||||
|
leaderboardType: string;
|
||||||
|
showPlayerStats: boolean;
|
||||||
|
showScoreInfo: boolean;
|
||||||
|
showSongInfo: boolean;
|
||||||
|
shouldReplacePlayerInfoWithScore: boolean;
|
||||||
|
setMounted: (isMounted: boolean) => void;
|
||||||
|
setOverlaySettings: (params: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useSettingsStore = create<SettingsState>()((set) => ({
|
||||||
|
mounted: false,
|
||||||
|
id: "",
|
||||||
|
socketAddr: "localhost",
|
||||||
|
leaderboardType: "ScoreSaber",
|
||||||
|
showPlayerStats: true,
|
||||||
|
showScoreInfo: false,
|
||||||
|
showSongInfo: false,
|
||||||
|
shouldReplacePlayerInfoWithScore: false,
|
||||||
|
|
||||||
|
setMounted: (isMounted: boolean) => set(() => ({ mounted: isMounted })),
|
||||||
|
setOverlaySettings: (params: any) =>
|
||||||
|
set(() => {
|
||||||
|
let values: any = {};
|
||||||
|
Object.entries(params).forEach((value) => {
|
||||||
|
if (value[0] !== undefined) {
|
||||||
|
if (value[1] == "true" || value[1] == "false") {
|
||||||
|
values[value[0]] = Utils.stringToBoolean(value[1]);
|
||||||
|
} else {
|
||||||
|
values[value[0]] = value[1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return values;
|
||||||
|
}),
|
||||||
|
}));
|
51
src/store/playerDataStore.ts
Normal file
51
src/store/playerDataStore.ts
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import axios from "axios";
|
||||||
|
import create from "zustand";
|
||||||
|
import Utils from "../utils/utils";
|
||||||
|
import { useSettingsStore } from "./overlaySettingsStore";
|
||||||
|
|
||||||
|
interface PlayerDataState {
|
||||||
|
isLoading: boolean;
|
||||||
|
id: string;
|
||||||
|
pp: number;
|
||||||
|
avatar: string;
|
||||||
|
globalPos: number;
|
||||||
|
countryRank: number;
|
||||||
|
country: string;
|
||||||
|
updatePlayerData: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const usePlayerDataStore = create<PlayerDataState>()((set) => ({
|
||||||
|
isLoading: true,
|
||||||
|
id: "",
|
||||||
|
pp: 0,
|
||||||
|
avatar: "",
|
||||||
|
globalPos: 0,
|
||||||
|
countryRank: 0,
|
||||||
|
country: "",
|
||||||
|
|
||||||
|
updatePlayerData: async () => {
|
||||||
|
const leaderboardType = useSettingsStore.getState().leaderboardType;
|
||||||
|
const playerId = useSettingsStore.getState().id;
|
||||||
|
|
||||||
|
const apiUrl = Utils.getWebsiteApi(
|
||||||
|
leaderboardType
|
||||||
|
).ApiUrl.PlayerData.replace("%s", playerId);
|
||||||
|
const response = await axios.get(apiUrl);
|
||||||
|
if (response.status !== 200) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const data = response.data;
|
||||||
|
|
||||||
|
console.log("Updated player data");
|
||||||
|
|
||||||
|
set(() => ({
|
||||||
|
id: playerId,
|
||||||
|
isLoading: false,
|
||||||
|
pp: data.pp,
|
||||||
|
avatar: data.avatar || data.profilePicture,
|
||||||
|
globalPos: data.rank,
|
||||||
|
countryRank: data.countryRank,
|
||||||
|
country: data.country,
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
}));
|
180
src/store/songDataStore.ts
Normal file
180
src/store/songDataStore.ts
Normal file
@ -0,0 +1,180 @@
|
|||||||
|
import env from "@beam-australia/react-env";
|
||||||
|
import axios from "axios";
|
||||||
|
import create from "zustand";
|
||||||
|
import Utils from "../utils/utils";
|
||||||
|
import { useSettingsStore } from "./overlaySettingsStore";
|
||||||
|
|
||||||
|
interface SongDataState {
|
||||||
|
isLoading: boolean;
|
||||||
|
hasError: boolean;
|
||||||
|
inSong: boolean;
|
||||||
|
songTitle: string;
|
||||||
|
songSubTitle: string;
|
||||||
|
songLength: number;
|
||||||
|
songDifficulty: string;
|
||||||
|
mapStarCount: number;
|
||||||
|
mapArt: string;
|
||||||
|
bsr: string;
|
||||||
|
|
||||||
|
paused: boolean;
|
||||||
|
failed: boolean;
|
||||||
|
currentSongTime: number;
|
||||||
|
currentScore: number;
|
||||||
|
percentage: string;
|
||||||
|
currentPP: number | undefined;
|
||||||
|
saberA: {
|
||||||
|
cutDistanceScore: number;
|
||||||
|
averagePreSwing: number;
|
||||||
|
averagePostSwing: number;
|
||||||
|
};
|
||||||
|
saberB: {
|
||||||
|
cutDistanceScore: number;
|
||||||
|
averagePreSwing: number;
|
||||||
|
averagePostSwing: number;
|
||||||
|
};
|
||||||
|
reset: () => void;
|
||||||
|
updateMapData: (
|
||||||
|
mapHash: string,
|
||||||
|
mapDiff: string,
|
||||||
|
characteristic: string,
|
||||||
|
songTitle: string,
|
||||||
|
songSubTitle: string,
|
||||||
|
songLength: number
|
||||||
|
) => void;
|
||||||
|
setFailed: (failed: boolean) => void;
|
||||||
|
setPaused: (paused: boolean) => void;
|
||||||
|
setCurrentScore: (score: number) => void;
|
||||||
|
setPercent: (percent: string) => void;
|
||||||
|
setPp: (pp: number) => void;
|
||||||
|
setInSong: (isInSong: boolean) => void;
|
||||||
|
setSaberData: (saberType: string, cutData: any) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useSongDataStore = create<SongDataState>()((set) => ({
|
||||||
|
isLoading: true,
|
||||||
|
hasError: false,
|
||||||
|
inSong: false,
|
||||||
|
songTitle: "",
|
||||||
|
songSubTitle: "",
|
||||||
|
songLength: 0,
|
||||||
|
songDifficulty: "",
|
||||||
|
mapStarCount: 0,
|
||||||
|
mapArt: "",
|
||||||
|
bsr: "",
|
||||||
|
|
||||||
|
paused: false,
|
||||||
|
failed: false,
|
||||||
|
currentSongTime: 0,
|
||||||
|
currentScore: 0,
|
||||||
|
percentage: "100%",
|
||||||
|
currentPP: undefined,
|
||||||
|
saberA: {
|
||||||
|
cutDistanceScore: 0.0,
|
||||||
|
averagePreSwing: 0.0,
|
||||||
|
averagePostSwing: 0.0,
|
||||||
|
},
|
||||||
|
saberB: {
|
||||||
|
cutDistanceScore: 0.0,
|
||||||
|
averagePreSwing: 0.0,
|
||||||
|
averagePostSwing: 0.0,
|
||||||
|
},
|
||||||
|
|
||||||
|
updateMapData: async (
|
||||||
|
mapHash: string,
|
||||||
|
mapDiff: string,
|
||||||
|
characteristic: string,
|
||||||
|
songTitle: string,
|
||||||
|
songSubTitle: string,
|
||||||
|
songLength: number
|
||||||
|
) => {
|
||||||
|
let hasError = false;
|
||||||
|
const leaderboardType = useSettingsStore.getState().leaderboardType;
|
||||||
|
|
||||||
|
const mapStars = await Utils.getWebsiteApi(leaderboardType).getMapStarCount(
|
||||||
|
mapHash,
|
||||||
|
mapDiff,
|
||||||
|
characteristic
|
||||||
|
);
|
||||||
|
|
||||||
|
const mapData = await axios.get(
|
||||||
|
`${env("SITE_URL")}/api/beatsaver/map?hash=${mapHash}`
|
||||||
|
);
|
||||||
|
if (mapData.status !== 200) {
|
||||||
|
return set({ isLoading: false, hasError: hasError });
|
||||||
|
}
|
||||||
|
const { bsr, mapArt } = mapData.data.data;
|
||||||
|
|
||||||
|
set({
|
||||||
|
isLoading: false,
|
||||||
|
hasError: hasError,
|
||||||
|
mapStarCount: mapStars,
|
||||||
|
bsr: bsr,
|
||||||
|
mapArt: mapArt,
|
||||||
|
songDifficulty: mapDiff,
|
||||||
|
songTitle: songTitle,
|
||||||
|
songSubTitle: songSubTitle,
|
||||||
|
songLength: songLength,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
setFailed: (failed: boolean) => {
|
||||||
|
set({ failed: failed });
|
||||||
|
},
|
||||||
|
|
||||||
|
setPaused: (paused: boolean) => {
|
||||||
|
set({ paused: paused });
|
||||||
|
},
|
||||||
|
|
||||||
|
setCurrentScore: (score: number) => {
|
||||||
|
set({ currentScore: score });
|
||||||
|
},
|
||||||
|
|
||||||
|
setPercent: (percent: string) => {
|
||||||
|
set({ percentage: percent });
|
||||||
|
},
|
||||||
|
|
||||||
|
setSaberData: (saberType: string, saberData: any) => {
|
||||||
|
if (saberType === "saberA") {
|
||||||
|
set({ saberA: saberData });
|
||||||
|
} else if (saberType === "saberB") {
|
||||||
|
set({ saberB: saberData });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
setPp: (pp: number) => {
|
||||||
|
set({ currentPP: pp });
|
||||||
|
},
|
||||||
|
|
||||||
|
setInSong: (isInSong: boolean) => {
|
||||||
|
set({ inSong: isInSong });
|
||||||
|
},
|
||||||
|
|
||||||
|
reset: () =>
|
||||||
|
set({
|
||||||
|
isLoading: true,
|
||||||
|
hasError: false,
|
||||||
|
songTitle: "",
|
||||||
|
songSubTitle: "",
|
||||||
|
songDifficulty: "",
|
||||||
|
mapStarCount: 0,
|
||||||
|
mapArt: "",
|
||||||
|
bsr: "",
|
||||||
|
|
||||||
|
paused: false,
|
||||||
|
failed: false,
|
||||||
|
currentSongTime: 0,
|
||||||
|
currentScore: 0,
|
||||||
|
percentage: "100%",
|
||||||
|
currentPP: undefined,
|
||||||
|
saberA: {
|
||||||
|
cutDistanceScore: 0.0,
|
||||||
|
averagePreSwing: 0.0,
|
||||||
|
averagePostSwing: 0.0,
|
||||||
|
},
|
||||||
|
saberB: {
|
||||||
|
cutDistanceScore: 0.0,
|
||||||
|
averagePreSwing: 0.0,
|
||||||
|
averagePostSwing: 0.0,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}));
|
@ -2,8 +2,5 @@
|
|||||||
html,
|
html,
|
||||||
body {
|
body {
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
|
margin: 0;
|
||||||
transition-property: all;
|
|
||||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
|
||||||
transition-duration: 150ms;
|
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
margin-left: 5px;
|
margin-left: 5px;
|
||||||
margin-top: 5px;
|
margin-top: 5px;
|
||||||
|
max-width: fit-content;
|
||||||
}
|
}
|
||||||
|
|
||||||
.playerStatsContainer p {
|
.playerStatsContainer p {
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
.scoreStats {
|
.scoreStats {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
position: absolute;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
|
margin-top: 5px;
|
||||||
margin-right: 5px;
|
margin-right: 5px;
|
||||||
min-width: 135px;
|
min-width: 135px;
|
||||||
|
|
||||||
|
@ -1,12 +1,9 @@
|
|||||||
.songInfoContainer {
|
.songInfoContainer {
|
||||||
--pos: 0;
|
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
bottom: var(--pos);
|
|
||||||
left: var(--pos);
|
|
||||||
margin-left: 5px;
|
margin-left: 5px;
|
||||||
margin-bottom: 5px;
|
margin-bottom: 5px;
|
||||||
|
margin-top: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.songInfoContainer img {
|
.songInfoContainer img {
|
||||||
@ -22,7 +19,7 @@
|
|||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
.songInfoSongAuthor {
|
.songInfoSongSubName {
|
||||||
font-size: 22px;
|
font-size: 22px;
|
||||||
margin-top: -12px;
|
margin-top: -12px;
|
||||||
margin-bottom: -3px;
|
margin-bottom: -3px;
|
||||||
|
@ -6,7 +6,7 @@ export default class Utils {
|
|||||||
/**
|
/**
|
||||||
* Returns the information for the given website type.
|
* Returns the information for the given website type.
|
||||||
*
|
*
|
||||||
* @param {LeaderboardType} website
|
* @param {string} website
|
||||||
* @returns The website type's information.
|
* @returns The website type's information.
|
||||||
*/
|
*/
|
||||||
static getWebsiteApi(website) {
|
static getWebsiteApi(website) {
|
||||||
@ -43,4 +43,23 @@ export default class Utils {
|
|||||||
static base64ToArrayBuffer(base64) {
|
static base64ToArrayBuffer(base64) {
|
||||||
return Uint8Array.from(atob(base64), (c) => c.charCodeAt(0));
|
return Uint8Array.from(atob(base64), (c) => c.charCodeAt(0));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static stringToBoolean = (stringValue) => {
|
||||||
|
switch (stringValue?.toLowerCase()?.trim()) {
|
||||||
|
case "true":
|
||||||
|
case "yes":
|
||||||
|
case "1":
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case "false":
|
||||||
|
case "no":
|
||||||
|
case "0":
|
||||||
|
case null:
|
||||||
|
case undefined:
|
||||||
|
return false;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return JSON.parse(stringValue);
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
@ -15,6 +15,12 @@
|
|||||||
"jsx": "preserve",
|
"jsx": "preserve",
|
||||||
"incremental": true
|
"incremental": true
|
||||||
},
|
},
|
||||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
|
"include": [
|
||||||
|
"next-env.d.ts",
|
||||||
|
"**/*.ts",
|
||||||
|
"**/*.tsx",
|
||||||
|
"src/helpers/web/getPlayerData.js",
|
||||||
|
"src/helpers/web/getPlayerData.js"
|
||||||
|
],
|
||||||
"exclude": ["node_modules"]
|
"exclude": ["node_modules"]
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user