diff --git a/components/ScoreStats.js b/components/ScoreStats.js
index f98814f..ec2d52d 100644
--- a/components/ScoreStats.js
+++ b/components/ScoreStats.js
@@ -4,135 +4,6 @@ export default class ScoreStats extends Component {
constructor(params) {
super(params);
-
- this.state = {
- socket: undefined,
- isVisible: true,
- currentScore: 0,
- percentage: "100.00%",
- failed: false,
- leftHand: {
- averageCut: [15.00],
- averagePreSwing: [70.00],
- averagePostSwing: [30.00],
- },
- rightHand: {
- averageCut: [15.00],
- averagePreSwing: [70.00],
- averagePostSwing: [30.00],
- }
- }
- }
-
- connectSocket() {
- const socket = new WebSocket('ws://localhost:6557/socket');
- socket.addEventListener('error' || 'close', () => {
- console.log("Attempting to re-connect to the HTTP Status socket in 30 seconds.");
- setTimeout(() => this.connectSocket(), 30_000);
- });
- socket.addEventListener('message', (message) => {
- const json = JSON.parse(message.data);
- if (!this.handlers[json.event]) {
- console.log("Unhandled message from HTTP Status. (" + json.event + ")");
- return;
- }
- this.handlers[json.event](json || []);
- })
- this.setState({ socket: socket });
- }
-
- resetData(visible) {
- console.log("Exiting level, resetting data.")
- this.setState({
- "leftHand": {
- "averageCut": [15.00],
- "averagePreSwing": [70.00],
- "averagePostSwing": [30.00],
- },
- "rightHand": {
- "averageCut": [15.00],
- "averagePreSwing": [70.00],
- "averagePostSwing": [30.00],
- },
- currentScore: 0,
- percentage: "100.00%",
- isVisible: visible
- });
- }
-
- handlers = {
- "hello": () => {
- console.log("Hello from HTTP Status!");
- },
- "scoreChanged": (data) => {
- const { status } = data;
- const { score, currentMaxScore } = status.performance;
- const percent = currentMaxScore > 0 ? ((score / currentMaxScore) * 1000 / 10).toFixed(2) : 0.00;
- this.setState({
- currentScore: score,
- percentage: this.state.failed ? percent * 2 : percent + "%"
- })
- },
- "noteFullyCut": (data) => {
- const { noteCut } = data;
-
- // Left Saber
- if (noteCut.saberType === 'SaberA') {
- const data = this.state.leftHand;
- if (data.averageCut.includes(15) && data.averageCut.length === 1) {
- data.averageCut = [];
- }
- if (data.averagePreSwing.includes(70) && data.averagePreSwing.length === 1) {
- data.averagePreSwing = [];
- }
- if (data.averagePostSwing.includes(30) && data.averagePostSwing.length === 1) {
- data.averagePostSwing = [];
- }
- data.averagePreSwing.push(noteCut.initialScore > 70 ? 70 : noteCut.initialScore);
- data.averagePostSwing.push(noteCut.finalScore - noteCut.initialScore);
- data.averageCut.push(noteCut.cutDistanceScore);
- this.setState({ leftHand: data });
- }
-
- // Left Saber
- if (noteCut.saberType === 'SaberB') {
- const data = this.state.rightHand;
- if (data.averageCut.includes(15) && data.averageCut.length === 1) {
- data.averageCut = [];
- }
- if (data.averagePreSwing.includes(70) && data.averagePreSwing.length === 1) {
- data.averagePreSwing = [];
- }
- if (data.averagePostSwing.includes(30) && data.averagePostSwing.length === 1) {
- data.averagePostSwing = [];
- }
- data.averagePreSwing.push(noteCut.initialScore > 70 ? 70 : noteCut.initialScore);
- data.averagePostSwing.push(noteCut.finalScore - noteCut.initialScore);
- data.averageCut.push(noteCut.cutDistanceScore);
- this.setState({ rightHand: data });
- }
- },
- "songStart": () => {
- console.log("Going into level, resetting data.")
- this.resetData(true);
- },
- "finished": () => {
- console.log("Exiting level, resetting data.")
- this.resetData(false);
- },
- "softFail": () => {
- this.setState({ failed: true });
- },
- "menu": () => {},
- "noteCut": () => {},
- "noteMissed": () => {},
- "noteSpawned": () => {},
- "bombMissed": () => {},
- "beatmapEvent": () => {}
- }
-
- componentDidMount() {
- this.connectSocket();
}
getAverage(values) {
@@ -140,24 +11,26 @@ export default class ScoreStats extends Component {
}
render() {
- return
+ const data = this.props.data;
+
+ return
Average Cut
-
{this.getAverage(this.state.leftHand.averagePreSwing).toFixed(2)}
-
{this.getAverage(this.state.leftHand.averagePostSwing).toFixed(2)}
-
{this.getAverage(this.state.leftHand.averageCut).toFixed(2)}
+
{this.getAverage(data.leftHand.averagePreSwing).toFixed(2)}
+
{this.getAverage(data.leftHand.averagePostSwing).toFixed(2)}
+
{this.getAverage(data.leftHand.averageCut).toFixed(2)}
-
{this.getAverage(this.state.rightHand.averagePreSwing).toFixed(2)}
-
{this.getAverage(this.state.rightHand.averagePostSwing).toFixed(2)}
-
{this.getAverage(this.state.rightHand.averageCut).toFixed(2)}
+
{this.getAverage(data.rightHand.averagePreSwing).toFixed(2)}
+
{this.getAverage(data.rightHand.averagePostSwing).toFixed(2)}
+
{this.getAverage(data.rightHand.averageCut).toFixed(2)}
-
{this.state.percentage}
-
{this.state.currentScore.toLocaleString()}
+
{data.percentage}
+
{data.currentScore.toLocaleString()}
}
diff --git a/components/SongInfo.js b/components/SongInfo.js
index 0ba706e..966dc20 100644
--- a/components/SongInfo.js
+++ b/components/SongInfo.js
@@ -1,14 +1,72 @@
import {Component} from "react";
+import Image from 'next/image'
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);
+ }
+
+ 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" });
+ }
+ }
+
+ msToMinSeconds(millis) {
+ console.log(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;
}
render() {
- return
+ const data = this.props.data.songData.status.beatmap;
+ const beatSaverData = this.props.data.beatSaverData;
+ const { id: bsr } = beatSaverData;
+ const songArt = beatSaverData.versions[0].coverURL;
+ const {
+ songName,
+ songAuthorName,
+ difficulty
+ } = data
+ const songTimerPercentage = ((this.props.data.currentSongTime / 1000) / (data.length / 1000)) * 100000;
+ return
+
+
+
{songName}
+
{songAuthorName}
+
+
{difficulty}
+
!bsr {bsr}
+
+
Song Time | {this.msToMinSeconds(this.props.data.currentSongTime * 1000)}/{this.msToMinSeconds(data.length)}
+
}
}
\ No newline at end of file
diff --git a/next.config.js b/next.config.js
index b169621..6538458 100644
--- a/next.config.js
+++ b/next.config.js
@@ -1,6 +1,10 @@
module.exports = {
reactStrictMode: true,
images: {
- domains: ['cdn.scoresaber.com'],
+ domains: [
+ 'cdn.scoresaber.com',
+ 'na.cdn.beatsaver.com',
+ 'eu.cdn.beatsaver.com'
+ ],
}
}
diff --git a/pages/index.js b/pages/index.js
index 46b8b8b..a692083 100644
--- a/pages/index.js
+++ b/pages/index.js
@@ -4,9 +4,11 @@ import PlayerStats from '../components/PlayerStats';
import ScoreStats from '../components/ScoreStats';
import Config from '../config.json';
+import SongInfo from "../components/SongInfo";
// Why do u have to proxy requests... it's so dumb LOL
-const API_URL = Config.proxy_url + "/https://scoresaber.com/api/player/%s/full";
+const SCORESABER_API_URL = Config.proxy_url + "/https://scoresaber.com/api/player/%s/full";
+const BEATSAVER_API_URL = Config.proxy_url + "/https://api.beatsaver.com/maps/hash/%s";
const GITHUB_URL = "https://github.com/RealFascinated/beatsaber-overlay";
export default class Home extends Component {
@@ -20,8 +22,39 @@ export default class Home extends Component {
isValidScoresaber: true,
data: undefined,
showPlayerStats: true,
- showScore: false
+ showScore: false,
+ showSongInfo: false,
+
+ socket: undefined,
+ isVisible: false,
+ songInfo: undefined,
+ beatSaverData: undefined,
+ currentSongTime: 0,
+ paused: true,
+ currentScore: 0,
+ percentage: "100.00%",
+ failed: false,
+ leftHand: {
+ averageCut: [15.00],
+ averagePreSwing: [70.00],
+ averagePostSwing: [30.00],
+ },
+ rightHand: {
+ averageCut: [15.00],
+ averagePreSwing: [70.00],
+ averagePostSwing: [30.00],
+ }
}
+ this.setupTimer();
+ }
+
+ // I'd love if HTTP Status just gave this data lmao
+ setupTimer() {
+ setInterval(() => {
+ if (!this.state.paused && this.state.beatSaverData !== undefined) {
+ this.setState({ currentSongTime: this.state.currentSongTime + 1 })
+ }
+ }, 1000);
}
async componentDidMount() {
@@ -39,9 +72,15 @@ export default class Home extends Component {
this.setState({ showPlayerStats: false });
}
- // Check if the player wants to show their current score information on the overlay
+ // Check if the player wants to show their current score information
if (params.scoreinfo === 'true') {
this.setState({ showScore: true });
+ this.connectSocket();
+ }
+
+ // Check if the player wants to show the current song
+ if (params.songinfo === 'true') {
+ this.setState({ showSongInfo: true });
}
await this.updateData(id);
@@ -54,21 +93,156 @@ export default class Home extends Component {
}
async updateData(id) {
- const data = await fetch(API_URL.replace("%s", id), {
+ const data = await fetch(SCORESABER_API_URL.replace("%s", id), {
mode: 'cors'
});
- if (data.status == 422) { // invalid scoresaber
+ if (data.status === 422) { // Invalid scoresaber account (I think??)
this.setState({ loading: false, isValidScoresaber: false });
return;
}
const json = await data.json();
- if (json.errorMessage) {
+ if (json.errorMessage) { // Invalid scoresaber account
this.setState({ loading: false, isValidScoresaber: false });
return;
}
this.setState({ loading: false, id: id, data: json });
}
+ connectSocket() {
+ const socket = new WebSocket('ws://localhost:6557/socket');
+ socket.addEventListener('error' || 'close', () => {
+ console.log("Attempting to re-connect to the HTTP Status socket in 30 seconds.");
+ setTimeout(() => this.connectSocket(), 30_000);
+ });
+ socket.addEventListener('message', (message) => {
+ const json = JSON.parse(message.data);
+ if (!this.handlers[json.event]) {
+ console.log("Unhandled message from HTTP Status. (" + json.event + ")");
+ return;
+ }
+ this.handlers[json.event](json || []);
+ })
+ this.setState({ socket: socket });
+ }
+
+ async setBeatSaver(songData) {
+ console.log("Updating BeatSaver info")
+ const data = await fetch(BEATSAVER_API_URL.replace("%s", songData.levelId.replace("custom_level_", "")));
+ const json = await data.json();
+ console.log(json)
+ this.setState({ beatSaverData: json })
+ }
+
+ resetData(visible) {
+ console.log("Exiting level, resetting data.")
+ this.setState({
+ "leftHand": {
+ "averageCut": [15.00],
+ "averagePreSwing": [70.00],
+ "averagePostSwing": [30.00],
+ },
+ "rightHand": {
+ "averageCut": [15.00],
+ "averagePreSwing": [70.00],
+ "averagePostSwing": [30.00],
+ },
+ songInfo: undefined,
+ beatSaverData: undefined,
+ currentSongTime: 0,
+ currentScore: 0,
+ percentage: "100.00%",
+ isVisible: visible
+ });
+ }
+
+ handlers = {
+ "hello": (data) => {
+ console.log("Hello from HTTP Status!");
+ if (data.status) {
+ this.setState({songData: data});
+ if (data.status.beatmap) {
+ this.setBeatSaver(data.status.beatmap);
+ }
+ }
+ },
+ "scoreChanged": (data) => {
+ const { status } = data;
+ const { score, currentMaxScore } = status.performance;
+ const percent = currentMaxScore > 0 ? ((score / currentMaxScore) * 1000 / 10).toFixed(2) : 0.00;
+ this.setState({
+ currentScore: score,
+ percentage: this.state.failed ? percent * 2 : percent + "%"
+ })
+ },
+ "noteFullyCut": (data) => {
+ const { noteCut } = data;
+
+ // Left Saber
+ if (noteCut.saberType === 'SaberA') {
+ const data = this.state.leftHand;
+ if (data.averageCut.includes(15) && data.averageCut.length === 1) {
+ data.averageCut = [];
+ }
+ if (data.averagePreSwing.includes(70) && data.averagePreSwing.length === 1) {
+ data.averagePreSwing = [];
+ }
+ if (data.averagePostSwing.includes(30) && data.averagePostSwing.length === 1) {
+ data.averagePostSwing = [];
+ }
+ data.averagePreSwing.push(noteCut.initialScore > 70 ? 70 : noteCut.initialScore);
+ data.averagePostSwing.push(noteCut.finalScore - noteCut.initialScore);
+ data.averageCut.push(noteCut.cutDistanceScore);
+ this.setState({ leftHand: data });
+ }
+
+ // Left Saber
+ if (noteCut.saberType === 'SaberB') {
+ const data = this.state.rightHand;
+ if (data.averageCut.includes(15) && data.averageCut.length === 1) {
+ data.averageCut = [];
+ }
+ if (data.averagePreSwing.includes(70) && data.averagePreSwing.length === 1) {
+ data.averagePreSwing = [];
+ }
+ if (data.averagePostSwing.includes(30) && data.averagePostSwing.length === 1) {
+ data.averagePostSwing = [];
+ }
+ data.averagePreSwing.push(noteCut.initialScore > 70 ? 70 : noteCut.initialScore);
+ data.averagePostSwing.push(noteCut.finalScore - noteCut.initialScore);
+ data.averageCut.push(noteCut.cutDistanceScore);
+ this.setState({ rightHand: data });
+ }
+ },
+ "songStart": (data) => {
+ console.log("Going into level, resetting data.")
+ this.resetData(true);
+ this.setState({ songData: data, paused: false })
+ this.setBeatSaver(data.status.beatmap);
+ },
+ "finished": () => {
+ console.log("Exiting level, resetting data.")
+ this.resetData(false);
+ },
+ "softFail": () => {
+ this.setState({ failed: true });
+ },
+ "paused": () => {
+ this.setState({ paused: true });
+ },
+ "resume": () => {
+ this.setState({ paused: false });
+ },
+ "menu": () => {
+ console.log("Exiting level, resetting data.")
+ this.resetData(false);
+ },
+ "noteCut": () => {},
+ "noteMissed": () => {},
+ "noteSpawned": () => {},
+ "bombMissed": () => {},
+ "beatmapEvent": () => {}
+ }
+
render() {
const { loading, isValidScoresaber, data } = this.state;
@@ -91,12 +265,14 @@ export default class Home extends Component {
Provide a valid scoresaber id
Example: {document.location.origin}?id=76561198449412074
Example with Score Info: {document.location.origin}?id=76561198449412074&scoreinfo=true
- Example with Score Info and without Player Stats: {document.location.origin}?id=76561198449412074&scoreinfo=true&playerstats=false
Options
scoreinfo - Can be "true" if you want to show your current score (needs HTTP Status)
playerstats - Can be "false" if you disable showing your stats (pp, global pos, etc)
+
songinfo - Can be "true" if want to see information about the song (song name, bsr, song art, etc)
+
+
To use a option just add &key=value (eg: &songinfo=true)
If you use this overlay and like it, don't forget to star the project :3
@@ -118,8 +294,10 @@ export default class Home extends Component {
""
}
{
- this.state.showScore ?
:
- ""
+ this.state.showScore && this.state.isVisible ?
: ""
+ }
+ {
+ this.state.showSongInfo && this.state.beatSaverData !== undefined && this.state.isVisible ?
: ""
}
}
diff --git a/styles/globals.css b/styles/globals.css
index abbeb77..4e2fb61 100644
--- a/styles/globals.css
+++ b/styles/globals.css
@@ -38,6 +38,8 @@
.player-stats-container {
display: flex;
+ margin-left: -5px;
+ margin-top: -5px;
}
.player-country {
@@ -73,7 +75,8 @@
position:absolute;
top:0;
right:0;
- margin-right: 10px;
+ margin-right: 5px;
+ margin-top: -5px;
}
.score-stats-right {
@@ -99,5 +102,82 @@
bottom:0;
right:0;
margin-right: 10px;
+ margin-bottom: -10px;
text-align: right;
-}
\ No newline at end of file
+}
+
+.song-info-container {
+ display: flex;
+ position: fixed;
+ bottom:0;
+ left:0;
+ margin-left: 10px;
+ margin-bottom: 10px;
+}
+
+.song-info-container img {
+ border-radius: 5%;
+}
+
+.song-info {
+ margin-left: 10px;
+ margin-top: -10px;
+}
+
+.song-info-song-name {
+ font-size: x-large;
+ font-weight: bold;
+}
+
+.song-info-song-author {
+ font-size: large;
+ margin-top: -5px;
+ margin-bottom: -5px;
+}
+
+.song-info-song-other-container {
+ display: flex;
+ margin-top: 5px;
+}
+
+.song-info-diff {
+ font-size: large;
+ padding: 9px 5px;
+ width: fit-content;
+ border-radius: 5%;
+ font-weight: bold;
+}
+
+.song-info-bsr {
+ font-size: large;
+ margin-left: 10px;
+ margin-top: 28px;
+}
+
+.song-time-container {
+ width: 200px;
+ display: grid;
+}
+
+.song-time-background {
+ grid-column: 1;
+ grid-row: 1;
+ z-index: 1;
+ padding: 3px;
+ width: 100%;
+ background-color: white !important;
+}
+
+.song-time {
+ grid-column: 1;
+ grid-row: 1;
+ z-index: 2;
+ padding: 3px;
+ background-color: red;
+}
+
+.song-time-text {
+ margin-top: 8px;
+ font-size: large;
+}
+