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; +} +