Refactored like the entire frontend
This commit is contained in:
parent
3e56a16b14
commit
b89f31a96e
@ -10,6 +10,7 @@ const nextConfig = {
|
||||
"cdn.scoresaber.com",
|
||||
"eu.cdn.beatsaver.com",
|
||||
"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/server": "^11.10.0",
|
||||
"@nextui-org/react": "^1.0.0-beta.10",
|
||||
"axios": "^1.1.3",
|
||||
"core-js-pure": "^3.26.0",
|
||||
"critters": "^0.0.16",
|
||||
"ioredis": "^5.2.3",
|
||||
@ -24,11 +25,14 @@
|
||||
"react-country-flag": "^3.0.2",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-toastify": "^9.0.8",
|
||||
"sharp": "^0.31.1"
|
||||
"sharp": "^0.31.1",
|
||||
"websocket": "^1.0.34",
|
||||
"zustand": "^4.1.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^18.11.7",
|
||||
"@types/react": "^18.0.24",
|
||||
"@types/websocket": "^1.0.5",
|
||||
"eslint": "8.26.0",
|
||||
"eslint-config-next": "12.3.1",
|
||||
"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 styles from "../styles/playerStats.module.css";
|
||||
import { useDataStore } from "../store/overlayDataStore";
|
||||
import { useSettingsStore } from "../store/overlaySettingsStore";
|
||||
import { usePlayerDataStore } from "../store/playerDataStore";
|
||||
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 (
|
||||
<div className={styles.playerStatsContainer}>
|
||||
<div>
|
||||
<Avatar url={props.avatar} />
|
||||
<Avatar url={avatar} />
|
||||
</div>
|
||||
<div className={styles.playerStats}>
|
||||
<p>
|
||||
{props.pp}pp{" "}
|
||||
{pp}pp{" "}
|
||||
<span
|
||||
style={{
|
||||
fontSize: "23px",
|
||||
}}
|
||||
>
|
||||
({props.websiteType})
|
||||
({leaderboardType})
|
||||
</span>
|
||||
</p>
|
||||
<p>#{props.globalPos}</p>
|
||||
<p>#{globalPos}</p>
|
||||
<div className={styles.playerCountry}>
|
||||
<p>#{props.countryRank}</p>
|
||||
<p>#{countryRank}</p>
|
||||
<ReactCountryFlag
|
||||
className={styles.playerCountryIcon}
|
||||
svg
|
||||
countryCode={props.country}
|
||||
countryCode={country}
|
||||
/>
|
||||
</div>
|
||||
{props.loadedDuringSong ? (
|
||||
{loadedDuringSong ? (
|
||||
<>
|
||||
<p className={styles.connectedDuringSong}>
|
||||
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 Utils from "../utils/utils";
|
||||
|
||||
export default class ScoreStats extends Component {
|
||||
constructor(params) {
|
||||
super(params);
|
||||
this.lastKnownPP = undefined;
|
||||
export default function ScoreStats() {
|
||||
const [showScoreInfo] = useSettingsStore((store) => [store.showScoreInfo]);
|
||||
const [percentage, currentScore, currentPP, saberA, saberB, isLoading] =
|
||||
useSongDataStore((store) => [
|
||||
store.percentage,
|
||||
store.currentScore,
|
||||
store.currentPP,
|
||||
store.saberA,
|
||||
store.saberB,
|
||||
store.isLoading,
|
||||
]);
|
||||
|
||||
if (isLoading) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
if (!showScoreInfo) {
|
||||
return null;
|
||||
}
|
||||
|
||||
render() {
|
||||
const data = this.props.data;
|
||||
let currentPP = Utils.calculatePP(
|
||||
data.mapStarCount,
|
||||
data.percentage.replace("%", ""),
|
||||
data.websiteType
|
||||
);
|
||||
if (this.lastKnownPP === undefined) {
|
||||
this.lastKnownPP = currentPP;
|
||||
}
|
||||
if (currentPP === undefined) {
|
||||
currentPP = this.lastKnownPP;
|
||||
}
|
||||
this.lastKnownPP = currentPP;
|
||||
|
||||
return (
|
||||
<div className={styles.scoreStats}>
|
||||
<div className={styles.scoreStatsInfo}>
|
||||
<p>{data.percentage}</p>
|
||||
<p>{data.currentScore.toLocaleString()}</p>
|
||||
<p>{percentage}</p>
|
||||
<p>{currentScore.toLocaleString()}</p>
|
||||
{currentPP !== undefined ? <p>{currentPP.toFixed(0)}pp</p> : null}
|
||||
</div>
|
||||
<p className={styles.scoreStatsAverageCut}>Average Cut</p>
|
||||
<div className={styles.scoreStatsHands}>
|
||||
<div className={styles.scoreStatsLeft}>
|
||||
<p>{data.SaberA.averagePreSwing.toFixed(2)}</p>
|
||||
<p>{data.SaberA.averagePostSwing.toFixed(2)}</p>
|
||||
<p>{data.SaberA.cutDistanceScore.toFixed(2)}</p>
|
||||
<p>{saberA.averagePreSwing.toFixed(2)}</p>
|
||||
<p>{saberA.averagePostSwing.toFixed(2)}</p>
|
||||
<p>{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>
|
||||
<p>{saberB.averagePreSwing.toFixed(2)}</p>
|
||||
<p>{saberB.averagePostSwing.toFixed(2)}</p>
|
||||
<p>{saberB.cutDistanceScore.toFixed(2)}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,80 +1,89 @@
|
||||
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";
|
||||
|
||||
export default class SongInfo extends Component {
|
||||
constructor(params) {
|
||||
super(params);
|
||||
this.state = {
|
||||
diffColor: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const data = this.props.data.songData.status.beatmap;
|
||||
this.formatDiff(data.difficulty);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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" });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* Format the given ms
|
||||
*
|
||||
* @param {Number} millis
|
||||
* @returns The formatted time
|
||||
*/
|
||||
msToMinSeconds(millis) {
|
||||
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") {
|
||||
return "#bf2a42";
|
||||
}
|
||||
if (diff === "Hard") {
|
||||
return "tomato";
|
||||
}
|
||||
if (diff === "Normal") {
|
||||
return "#59b0f4";
|
||||
}
|
||||
if (diff === "Easy") {
|
||||
return "MediumSeaGreen";
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
render() {
|
||||
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;
|
||||
if (isLoading) {
|
||||
return null;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
const songTimerPercentage = (currentSongTime / songLength) * 100000;
|
||||
const diffColor = formatDiff(songDifficulty);
|
||||
|
||||
return (
|
||||
<div className={styles.songInfoContainer}>
|
||||
<div
|
||||
className={styles.songInfoContainer}
|
||||
style={{
|
||||
bottom: shouldReplacePlayerInfoWithScore ? "" : 0,
|
||||
left: shouldReplacePlayerInfoWithScore ? "" : 0,
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
width={150}
|
||||
height={150}
|
||||
@ -86,23 +95,22 @@ export default class SongInfo extends Component {
|
||||
/>
|
||||
<div className={styles.songInfo}>
|
||||
<p className={styles.songInfoSongName}>
|
||||
{songName.length > 35
|
||||
? songName.substring(0, 35) + "..."
|
||||
: songName}
|
||||
{songTitle.length > 35
|
||||
? songTitle.substring(0, 35) + "..."
|
||||
: songTitle}
|
||||
</p>
|
||||
<p className={styles.songInfoSongAuthor}>{songAuthorName}</p>
|
||||
<p className={styles.songInfoSongSubName}>{songSubTitle}</p>
|
||||
<div className={styles.songInfoSongOtherContainer}>
|
||||
<p
|
||||
className={styles.songInfoDiff}
|
||||
style={{ backgroundColor: this.state.diffColor }}
|
||||
style={{ backgroundColor: diffColor }}
|
||||
>
|
||||
{difficulty}
|
||||
{songDifficulty.replace("Plus", "+")}
|
||||
</p>
|
||||
<p className={styles.songInfoBsr}>!bsr {bsr}</p>
|
||||
</div>
|
||||
<p className={styles.songTimeText}>
|
||||
{this.msToMinSeconds(this.props.data.currentSongTime * 1000)}/
|
||||
{this.msToMinSeconds(data.length)}
|
||||
{msToMinSeconds(currentSongTime * 1000)}/{msToMinSeconds(songLength)}
|
||||
</p>
|
||||
<div className={styles.songTimeContainer}>
|
||||
<div className={styles.songTimeBackground} />
|
||||
@ -114,5 +122,4 @@ export default class SongInfo extends Component {
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
import env from "@beam-australia/react-env";
|
||||
import { VARS } from "./EnvVars";
|
||||
|
||||
const WebsiteTypes = {
|
||||
const LeaderboardType = {
|
||||
ScoreSaber: {
|
||||
ApiUrl: {
|
||||
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: {
|
||||
socketAddr: undefined,
|
||||
leaderboard: "ScoreSaber",
|
||||
leaderboardType: "ScoreSaber",
|
||||
showPlayerStats: true,
|
||||
showScoreInfo: false,
|
||||
showSongInfo: false,
|
||||
@ -72,7 +72,10 @@ export default class Home extends Component {
|
||||
const json = JSON.parse(localStorage.getItem("values"));
|
||||
let values = {};
|
||||
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];
|
||||
}
|
||||
});
|
||||
@ -96,10 +99,6 @@ export default class Home extends Component {
|
||||
if (value[1] === undefined) {
|
||||
return;
|
||||
}
|
||||
if (value[0] == "leaderboard" && value[1] === "BeatLeader") {
|
||||
values += `&beatLeader=true`;
|
||||
return;
|
||||
}
|
||||
values += `&${value[0]}=${value[1]}`;
|
||||
});
|
||||
|
||||
@ -234,9 +233,11 @@ export default class Home extends Component {
|
||||
<Spacer y={1} />
|
||||
<Text>Ranked leaderboard</Text>
|
||||
<Radio.Group
|
||||
defaultValue={this.state.values.leaderboard || "ScoreSaber"}
|
||||
defaultValue={
|
||||
this.state.values.leaderboardType || "ScoreSaber"
|
||||
}
|
||||
onChange={(value) => {
|
||||
this.updateValue("leaderboard", value);
|
||||
this.updateValue("leaderboardType", value);
|
||||
}}
|
||||
>
|
||||
<Radio
|
||||
|
@ -1,516 +1,78 @@
|
||||
import { Link, Spinner } from "@nextui-org/react";
|
||||
import { NextSeo } from "next-seo";
|
||||
import { Component } from "react";
|
||||
import PlayerStats from "../../src/components/PlayerStats";
|
||||
import ScoreStats from "../../src/components/ScoreStats";
|
||||
import SongInfo from "../../src/components/SongInfo";
|
||||
import LeaderboardType from "../../src/consts/LeaderboardType";
|
||||
import Utils from "../../src/utils/utils";
|
||||
import axios from "axios";
|
||||
import { useEffect } from "react";
|
||||
import PlayerStats from "../components/PlayerStats";
|
||||
import ScoreStats from "../components/ScoreStats";
|
||||
import SongInfo from "../components/SongInfo";
|
||||
import { connectClient } from "../helpers/websocketClient";
|
||||
import { useSettingsStore } from "../store/overlaySettingsStore";
|
||||
import { usePlayerDataStore } from "../store/playerDataStore";
|
||||
|
||||
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",
|
||||
},
|
||||
}
|
||||
export default function Overlay(props) {
|
||||
const query = JSON.parse(props.query);
|
||||
const [setOverlaySettings, mounted, setMounted] = useSettingsStore(
|
||||
(state) => [state.setOverlaySettings, state.mounted, state.setMounted]
|
||||
);
|
||||
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."
|
||||
const updatePlayerData = usePlayerDataStore(
|
||||
(state) => state.updatePlayerData
|
||||
);
|
||||
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)
|
||||
useEffect(() => {
|
||||
if (!mounted && props.isValidSteamId) {
|
||||
setMounted(true);
|
||||
|
||||
async function setup() {
|
||||
await setOverlaySettings(query);
|
||||
const showSongInfo = useSettingsStore.getState().showSongInfo;
|
||||
const showScoreInfo = useSettingsStore.getState().showScoreInfo;
|
||||
if (showSongInfo || (showScoreInfo && typeof window !== "undefined")) {
|
||||
await connectClient();
|
||||
}
|
||||
const showPlayerStats = useSettingsStore.getState().showPlayerStats;
|
||||
if (showPlayerStats) {
|
||||
await updatePlayerData();
|
||||
}
|
||||
}
|
||||
setup();
|
||||
}
|
||||
}, [
|
||||
query,
|
||||
props.isValidSteamId,
|
||||
setOverlaySettings,
|
||||
mounted,
|
||||
setMounted,
|
||||
updatePlayerData,
|
||||
]);
|
||||
|
||||
if (!props.isValidSteamId) {
|
||||
return (
|
||||
<div className={styles.invalidPlayer}>
|
||||
<h1>Invalid Steam ID</h1>
|
||||
<h3>Please check the id field in the url</h3>
|
||||
</div>
|
||||
);
|
||||
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>
|
||||
<PlayerStats />
|
||||
<ScoreStats />
|
||||
<SongInfo />
|
||||
</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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
body {
|
||||
background-color: transparent;
|
||||
|
||||
transition-property: all;
|
||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transition-duration: 150ms;
|
||||
margin: 0;
|
||||
}
|
||||
|
@ -2,6 +2,7 @@
|
||||
display: flex;
|
||||
margin-left: 5px;
|
||||
margin-top: 5px;
|
||||
max-width: fit-content;
|
||||
}
|
||||
|
||||
.playerStatsContainer p {
|
||||
|
@ -1,8 +1,9 @@
|
||||
.scoreStats {
|
||||
text-align: center;
|
||||
position: absolute;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
margin-top: 5px;
|
||||
margin-right: 5px;
|
||||
min-width: 135px;
|
||||
|
||||
|
@ -1,12 +1,9 @@
|
||||
.songInfoContainer {
|
||||
--pos: 0;
|
||||
|
||||
display: flex;
|
||||
position: fixed;
|
||||
bottom: var(--pos);
|
||||
left: var(--pos);
|
||||
margin-left: 5px;
|
||||
margin-bottom: 5px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.songInfoContainer img {
|
||||
@ -22,7 +19,7 @@
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.songInfoSongAuthor {
|
||||
.songInfoSongSubName {
|
||||
font-size: 22px;
|
||||
margin-top: -12px;
|
||||
margin-bottom: -3px;
|
||||
|
@ -6,7 +6,7 @@ export default class Utils {
|
||||
/**
|
||||
* Returns the information for the given website type.
|
||||
*
|
||||
* @param {LeaderboardType} website
|
||||
* @param {string} website
|
||||
* @returns The website type's information.
|
||||
*/
|
||||
static getWebsiteApi(website) {
|
||||
@ -43,4 +43,23 @@ export default class Utils {
|
||||
static base64ToArrayBuffer(base64) {
|
||||
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",
|
||||
"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"]
|
||||
}
|
||||
|
Reference in New Issue
Block a user