commit
3f90d6f926
@ -17,6 +17,7 @@
|
|||||||
"react": "17.0.2",
|
"react": "17.0.2",
|
||||||
"react-country-flag": "^3.0.2",
|
"react-country-flag": "^3.0.2",
|
||||||
"react-dom": "17.0.2",
|
"react-dom": "17.0.2",
|
||||||
|
"react-toastify": "^9.0.8",
|
||||||
"sharp": "^0.30.1"
|
"sharp": "^0.30.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
@ -4,7 +4,7 @@ import Head from 'next/head'
|
|||||||
|
|
||||||
import Config from '../config.json';
|
import Config from '../config.json';
|
||||||
|
|
||||||
import '../styles/globals.css'
|
import '../styles/globals.css';
|
||||||
|
|
||||||
function MyApp({ Component, pageProps }) {
|
function MyApp({ Component, pageProps }) {
|
||||||
return (
|
return (
|
||||||
|
@ -22,7 +22,7 @@ export default async function handler(req, res) {
|
|||||||
fs.writeFileSync(imagePath, buffer);
|
fs.writeFileSync(imagePath, buffer);
|
||||||
res.setHeader('Content-Type', 'image/' + ext);
|
res.setHeader('Content-Type', 'image/' + ext);
|
||||||
res.send(buffer);
|
res.send(buffer);
|
||||||
console.log("Song Cache - Added song \"" + mapHash + "\"");
|
console.log("Song Art Cache - Added song \"" + mapHash + "\"");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const buffer = fs.readFileSync(imagePath);
|
const buffer = fs.readFileSync(imagePath);
|
||||||
|
471
pages/index.js
471
pages/index.js
@ -1,363 +1,188 @@
|
|||||||
import {Component} from 'react'
|
import { Button, Card, Container, Grid, Input, Link, Modal, Spacer, Switch, Text, textTransforms } from '@nextui-org/react';
|
||||||
import Avatar from '../src/components/Avatar';
|
import { Component } from 'react';
|
||||||
import PlayerStats from '../src/components/PlayerStats';
|
import NavBar from '../src/components/Navbar';
|
||||||
import ScoreStats from '../src/components/ScoreStats';
|
|
||||||
import SongInfo from "../src/components/SongInfo";
|
|
||||||
|
|
||||||
import Utils from '../src/utils/utils';
|
import { ToastContainer, toast } from 'react-toastify';
|
||||||
|
import 'react-toastify/dist/ReactToastify.css';
|
||||||
|
|
||||||
|
import styles from '../styles/main.module.css';
|
||||||
|
|
||||||
export default class Home extends Component {
|
export default class Home extends Component {
|
||||||
|
|
||||||
#_beatSaverURL = "";
|
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
loading: true,
|
steamId: undefined,
|
||||||
id: undefined,
|
isPreviewVisible: false,
|
||||||
isValidSteamId: true,
|
previewUrl: undefined,
|
||||||
websiteType: "ScoreSaber",
|
overlayUrl: undefined,
|
||||||
data: undefined,
|
|
||||||
showPlayerStats: true,
|
|
||||||
showScore: false,
|
|
||||||
showSongInfo: false,
|
|
||||||
|
|
||||||
socket: undefined,
|
values: {
|
||||||
isVisible: false,
|
socketAddr: undefined,
|
||||||
songInfo: undefined,
|
useBeatLeader: false,
|
||||||
beatSaverData: undefined,
|
showPlayerStats: true,
|
||||||
currentSongTime: 0,
|
showScoreInfo: false,
|
||||||
paused: true,
|
showSongInfo: false,
|
||||||
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
|
|
||||||
// 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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async componentDidMount() {
|
async componentDidMount() {
|
||||||
this.#_beatSaverURL = document.location.origin + "/api/beatsaver/map?hash=%s";
|
|
||||||
const urlSearchParams = new URLSearchParams(window.location.search);
|
const urlSearchParams = new URLSearchParams(window.location.search);
|
||||||
const params = Object.fromEntries(urlSearchParams.entries());
|
const params = Object.fromEntries(urlSearchParams.entries());
|
||||||
|
|
||||||
// Check if the player wants to disable their stats (pp, global pos, etc)
|
if (params.id) {
|
||||||
if (params.beatleader === 'true') {
|
document.location.href = "/overlay/"+ window.location.search
|
||||||
this.setState({ websiteType: "BeatLeader" });
|
|
||||||
}
|
|
||||||
|
|
||||||
const id = params.id;
|
|
||||||
if (!id) { // Check if the id param is valid
|
|
||||||
this.setState({ loading: false, isValidSteamId: false });
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if the player wants to disable their stats (pp, global pos, etc)
|
if (localStorage.getItem('values') == undefined) {
|
||||||
if (params.playerstats === 'false') {
|
localStorage.setItem('values', JSON.stringify({
|
||||||
this.setState({ showPlayerStats: false });
|
steamId: this.state.steamId,
|
||||||
}
|
values: this.state.values
|
||||||
|
}));
|
||||||
setTimeout(async () => {
|
} else {
|
||||||
await this.updateData(id);
|
const json = JSON.parse(localStorage.getItem('values'))
|
||||||
}, 10); // 10ms
|
this.setState({ steamId: json.steamId, values: json.values });
|
||||||
|
|
||||||
let shouldConnectSocket = false;
|
|
||||||
|
|
||||||
// Check if the player wants to show their current score information
|
|
||||||
if (params.scoreinfo === 'true') {
|
|
||||||
this.setState({ showScore: true });
|
|
||||||
shouldConnectSocket = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if the player wants to show the current song
|
|
||||||
if (params.songinfo === 'true') {
|
|
||||||
this.setState({ showSongInfo: true });
|
|
||||||
shouldConnectSocket = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`shouldConnectSocket = ${shouldConnectSocket}`);
|
|
||||||
if (shouldConnectSocket) {
|
|
||||||
this.connectSocket(params.socketaddress);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
loadPreview() {
|
||||||
* Fetch and update the data from the respective platform
|
this.setState({ isPreviewVisible: true, previewUrl: this.generateUrl(true) });
|
||||||
*
|
|
||||||
* @param {string} id The steam id of the player
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
async updateData(id) {
|
|
||||||
const data = await fetch(new Utils().getWebsiteApi(id == "test" ? "Test" : this.state.websiteType).ApiUrl.replace("%s", id), {
|
|
||||||
mode: 'cors'
|
|
||||||
});
|
|
||||||
const json = await data.json();
|
|
||||||
if (json.errorMessage) { // Invalid steam account
|
|
||||||
this.setState({ loading: false, isValidSteamId: false });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.setState({ loading: false, id: id, data: json });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
generateUrl(withTc = false) {
|
||||||
* Setup the HTTP Status connection
|
let values = "";
|
||||||
*/
|
Object.entries(this.state.values).forEach(value => {
|
||||||
connectSocket(socketAddress) {
|
if (value[1] === undefined) {
|
||||||
socketAddress = socketAddress === undefined ? 'ws://localhost' : `ws://${socketAddress}:6557/socket`;
|
|
||||||
console.log(`Connecting to ${socketAddress}`);
|
|
||||||
const socket = new WebSocket(socketAddress);
|
|
||||||
socket.addEventListener('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);
|
|
||||||
this.handleCurrentSongTime(json)
|
|
||||||
if (!this.handlers[json.event]) {
|
|
||||||
console.log("Unhandled message from HTTP Status. (" + json.event + ")");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.handlers[json.event](json || []);
|
if (value[0] == "useBeatLeader" && value[1] === true) {
|
||||||
})
|
values += `&beatLeader=${value[1]}`;
|
||||||
this.setState({ socket: socket });
|
return;
|
||||||
}
|
}
|
||||||
|
values += `&${value[0]}=${value[1]}`;
|
||||||
/**
|
|
||||||
* 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 })
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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) {
|
|
||||||
console.log("Exiting level, resetting data.")
|
|
||||||
setTimeout(async () => {
|
|
||||||
await this.updateData(id);
|
|
||||||
}, 250);
|
|
||||||
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
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return window.location.origin + "/overlay?id=" + this.state.steamId + values + (withTc ? "&textColor=black" : "");
|
||||||
}
|
}
|
||||||
|
|
||||||
// The HTTP Status handlers
|
updateValue(key, value) {
|
||||||
handlers = {
|
let values = this.state.values;
|
||||||
"hello": (data) => {
|
values[key] = value;
|
||||||
console.log("Hello from HTTP Status!");
|
this.setState({ values: values });
|
||||||
if (data.status) {
|
this.updateStorage();
|
||||||
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
|
updateStorage() {
|
||||||
if (noteCut.saberType === 'SaberA') {
|
setTimeout(() => {
|
||||||
const data = this.state.leftHand;
|
localStorage.setItem('values', JSON.stringify({
|
||||||
if (data.averageCut.includes(15) && data.averageCut.length === 1) {
|
steamId: this.state.steamId,
|
||||||
data.averageCut = [];
|
values: this.state.values
|
||||||
}
|
}));
|
||||||
if (data.averagePreSwing.includes(70) && data.averagePreSwing.length === 1) {
|
}, 5);
|
||||||
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": () => {
|
|
||||||
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": () => {}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { loading, isValidSteamId, data, websiteType } = this.state;
|
return <div className={styles.main}>
|
||||||
|
<NavBar></NavBar>
|
||||||
|
|
||||||
// When in the main menu, show this colour so it's actually readable
|
<Container css={{
|
||||||
if (!isValidSteamId && !loading) {
|
marginTop: '$8'
|
||||||
const body = document.body;
|
}}>
|
||||||
body.style.backgroundColor = "#181a1b";
|
|
||||||
}
|
|
||||||
|
|
||||||
return <>
|
{/* Preview */}
|
||||||
{ loading ?
|
|
||||||
<div className={'loading'}>
|
|
||||||
<h2>Loading...</h2>
|
|
||||||
</div>
|
|
||||||
: !isValidSteamId ?
|
|
||||||
<div className={'invalid-player'}>
|
|
||||||
<h1>BeatSaber Overlay</h1>
|
|
||||||
<div style={{ fontWeight: 'bold', marginBottom: '50px' }}>
|
|
||||||
<p>This is currently just a simple overlay for OBS displaying ScoreSaber or BeatLeader stats.</p>
|
|
||||||
<p>If you have any suggestions you can message me on discord @ Fascinated#4719</p>
|
|
||||||
</div>
|
|
||||||
<p>Provide a valid steam id for scoresaber or beatleader</p>
|
|
||||||
<p>Example: {document.location.origin}?id=76561198449412074</p>
|
|
||||||
<p>Example with Score Info: {document.location.origin}?id=76561198449412074&scoreinfo=true</p>
|
|
||||||
<p>Example with Multiple PCs: {document.location.origin}?id=76561198449412074&scoreinfo=true&socketaddress=192.168.1.15</p>
|
|
||||||
<div className={'info'}>
|
|
||||||
<div>
|
|
||||||
<h3>Options</h3>
|
|
||||||
<p><b>beatleader</b> - Can be "true" if you wish to get player data from BeatLeader rather than scoresaber</p>
|
|
||||||
<p><b>scoreinfo</b> - Can be "true" if you want to show your current score (needs HTTP Status)</p>
|
|
||||||
<p><b>playerstats</b> - Can be "false" if you disable showing your stats (pp, global pos, etc)</p>
|
|
||||||
<p><b>songinfo</b> - Can be "true" if want to see information about the song (song name, bsr, song art, etc)</p>
|
|
||||||
<p><b>socketaddress</b> - If you use multiple computers to stream (main pc, streaming pc) then this is for you.</p>
|
|
||||||
<p>You can set it to the local address of the pc (eg: 192.168.1.15)</p>
|
|
||||||
<br />
|
|
||||||
<p>To use a option just add &key=value (eg: &songinfo=true)</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div> :
|
|
||||||
<div className={'overlay'}>
|
|
||||||
{ this.state.showPlayerStats ?
|
|
||||||
<div className={'player-stats-container'}>
|
|
||||||
<Avatar url={data.profilePicture || data.avatar} />
|
|
||||||
<PlayerStats
|
|
||||||
pp={data.pp.toLocaleString()}
|
|
||||||
globalPos={data.rank.toLocaleString()}
|
|
||||||
country={data.country}
|
|
||||||
countryRank={data.countryRank.toLocaleString()}
|
|
||||||
websiteType={websiteType}
|
|
||||||
/>
|
|
||||||
</div> :
|
|
||||||
""
|
|
||||||
}
|
|
||||||
{
|
{
|
||||||
this.state.showScore && this.state.isVisible ? <ScoreStats data={this.state} /> : ""
|
this.state.isPreviewVisible ? <Modal
|
||||||
|
closeButton
|
||||||
|
open={this.state.isPreviewVisible}
|
||||||
|
width={"100%"}
|
||||||
|
blur
|
||||||
|
onClose={() => this.setState({ isPreviewVisible: false })}
|
||||||
|
>
|
||||||
|
<Modal.Header>
|
||||||
|
<Text size={18}>
|
||||||
|
Overlay Preview
|
||||||
|
</Text>
|
||||||
|
</Modal.Header>
|
||||||
|
<Modal.Body>
|
||||||
|
<iframe height={600} src={this.state.previewUrl}></iframe>
|
||||||
|
</Modal.Body>
|
||||||
|
</Modal> : <></>
|
||||||
}
|
}
|
||||||
{
|
|
||||||
this.state.showSongInfo && this.state.beatSaverData !== undefined && this.state.isVisible ? <SongInfo data={this.state}/> : ""
|
<Grid.Container gap={2}>
|
||||||
}
|
<Grid xs={12}>
|
||||||
</div>
|
<Card>
|
||||||
}
|
<Card.Body>
|
||||||
</>
|
<Text>Welcome to the Setup panel</Text>
|
||||||
|
|
||||||
|
<Spacer y={2} />
|
||||||
|
|
||||||
|
<Input
|
||||||
|
underlined
|
||||||
|
labelPlaceholder="Ip Address (Only set if you stream on multiple devices)"
|
||||||
|
initialValue="localhost"
|
||||||
|
value={this.state.values.socketAddr}
|
||||||
|
onChange={event => this.updateValue("socketAddr", event.target.value)} checked={true}
|
||||||
|
/>
|
||||||
|
<Spacer y={2} />
|
||||||
|
<Input
|
||||||
|
underlined
|
||||||
|
labelPlaceholder="Steam Id"
|
||||||
|
initialValue=""
|
||||||
|
value={this.state.steamId}
|
||||||
|
onChange={event => {
|
||||||
|
this.setState({ steamId: event.target.value });
|
||||||
|
this.updateStorage();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Spacer y={1} />
|
||||||
|
<Text>Do you want to use BeatLeader rather than ScoreSaber?</Text>
|
||||||
|
<Switch onChange={event => this.updateValue("useBeatLeader", event.target.checked)} checked={this.state.values.useBeatLeader} size="md" />
|
||||||
|
<Text>Do you want to show Player Stats (Current PP, global pos, etc)</Text>
|
||||||
|
<Switch onChange={event => this.updateValue("showPlayerStats", event.target.checked)} checked={this.state.values.showPlayerStats} size="md" />
|
||||||
|
<Text>Do you want to show Score Info (Current swing values, total score, etc)</Text>
|
||||||
|
<Switch onChange={event => this.updateValue("showScoreInfo", event.target.checked)} checked={this.state.values.showScoreInfo} size="md" />
|
||||||
|
<Text>Do you want to show Song Info (Song name, bsr, song art, etc)</Text>
|
||||||
|
<Switch onChange={event => this.updateValue("showSongInfo", event.target.checked)} checked={this.state.values.showSongInfo} size="md" />
|
||||||
|
<Spacer y={2} />
|
||||||
|
|
||||||
|
<Button.Group>
|
||||||
|
<Button flat auto onClick={() => {
|
||||||
|
if (!this.state.steamId) {
|
||||||
|
toast.error("Please provide a Steam ID");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
window.open(this.generateUrl(), "_blank");
|
||||||
|
}}>Open Overlay</Button>
|
||||||
|
<Button flat auto onClick={() => {
|
||||||
|
if (!this.state.steamId) {
|
||||||
|
toast.error("Please provide a Steam ID");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.loadPreview();
|
||||||
|
}}>Preview</Button>
|
||||||
|
</Button.Group>
|
||||||
|
|
||||||
|
{
|
||||||
|
this.state.overlayUrl !== undefined ?
|
||||||
|
<>
|
||||||
|
<Text b>
|
||||||
|
Url
|
||||||
|
</Text>
|
||||||
|
<Link href={this.state.overlayUrl}>{this.state.overlayUrl}</Link>
|
||||||
|
</>
|
||||||
|
: <></>
|
||||||
|
}
|
||||||
|
</Card.Body>
|
||||||
|
</Card>
|
||||||
|
</Grid>
|
||||||
|
</Grid.Container>
|
||||||
|
</Container>
|
||||||
|
|
||||||
|
<ToastContainer />
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
354
pages/overlay.js
Normal file
354
pages/overlay.js
Normal file
@ -0,0 +1,354 @@
|
|||||||
|
import { Link } from '@nextui-org/react';
|
||||||
|
import {Component} from 'react'
|
||||||
|
import PlayerStats from '../src/components/PlayerStats';
|
||||||
|
import ScoreStats from '../src/components/ScoreStats';
|
||||||
|
import SongInfo from "../src/components/SongInfo";
|
||||||
|
|
||||||
|
import Utils from '../src/utils/utils';
|
||||||
|
import styles from '../styles/overlay.module.css';
|
||||||
|
|
||||||
|
export default class Overlay extends Component {
|
||||||
|
|
||||||
|
#_beatSaverURL = "";
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
loading: true,
|
||||||
|
id: undefined,
|
||||||
|
isValidSteamId: true,
|
||||||
|
websiteType: "ScoreSaber",
|
||||||
|
data: undefined,
|
||||||
|
showPlayerStats: true,
|
||||||
|
showScore: false,
|
||||||
|
showSongInfo: false,
|
||||||
|
textColor: undefined,
|
||||||
|
|
||||||
|
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
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async componentDidMount() {
|
||||||
|
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') {
|
||||||
|
this.setState({ websiteType: "BeatLeader" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const id = params.id;
|
||||||
|
if (!id) { // Check if the id param is valid
|
||||||
|
this.setState({ loading: false, isValidSteamId: false });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the player wants to disable their stats (pp, global pos, etc)
|
||||||
|
if (params.playerstats === 'false') {
|
||||||
|
this.setState({ showPlayerStats: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(async () => {
|
||||||
|
await this.updateData(id);
|
||||||
|
}, 10); // 10ms
|
||||||
|
|
||||||
|
let shouldConnectSocket = false;
|
||||||
|
|
||||||
|
// Check if the player wants to show their current score information
|
||||||
|
if (params.scoreinfo === 'true') {
|
||||||
|
this.setState({ showScore: true });
|
||||||
|
shouldConnectSocket = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the player wants to show the current song
|
||||||
|
if (params.songinfo === 'true') {
|
||||||
|
this.setState({ showSongInfo: true });
|
||||||
|
shouldConnectSocket = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mainly used for the preview
|
||||||
|
if (params.textColor) {
|
||||||
|
this.setState({ textColor: params.textColor });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldConnectSocket) {
|
||||||
|
this.connectSocket(params.socketaddress);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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(new Utils().getWebsiteApi(id == "test" ? "Test" : this.state.websiteType).ApiUrl.replace("%s", id), {
|
||||||
|
mode: 'cors'
|
||||||
|
});
|
||||||
|
const json = await data.json();
|
||||||
|
if (json.errorMessage) { // Invalid account
|
||||||
|
this.setState({ loading: false, isValidSteamId: false });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.setState({ loading: false, id: id, data: json });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setup the HTTP Status connection
|
||||||
|
*/
|
||||||
|
connectSocket(socketAddress) {
|
||||||
|
socketAddress = (socketAddress === undefined ? 'ws://localhost' : `ws://${socketAddress}`) + ":6557/socket";
|
||||||
|
console.log(`Connecting to ${socketAddress}`);
|
||||||
|
const socket = new WebSocket(socketAddress);
|
||||||
|
socket.addEventListener('open', () => {
|
||||||
|
console.log(`Connected to ${socketAddress}`);
|
||||||
|
});
|
||||||
|
socket.addEventListener('close', () => {
|
||||||
|
console.log("Attempting to re-connect to the HTTP Status socket in 10 seconds.");
|
||||||
|
setTimeout(() => this.connectSocket(), 10_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 })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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) {
|
||||||
|
console.log("Exiting level, resetting data.")
|
||||||
|
setTimeout(async () => {
|
||||||
|
await this.updateData(this.state.id);
|
||||||
|
}, 250);
|
||||||
|
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
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// The HTTP Status handlers
|
||||||
|
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": () => {
|
||||||
|
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": () => {},
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { loading, isValidSteamId, data, websiteType } = this.state;
|
||||||
|
|
||||||
|
if (this.state.textColor !== undefined) {
|
||||||
|
const element = document.querySelector("." + styles.main);
|
||||||
|
element.style.color = this.state.textColor
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div className={styles.main}>
|
||||||
|
{ loading ?
|
||||||
|
<div className={styles.loading}>
|
||||||
|
<h2>Loading...</h2>
|
||||||
|
</div>
|
||||||
|
: !isValidSteamId ?
|
||||||
|
<div className={styles.invalidPlayer}>
|
||||||
|
<h1>Invalid player, please visit the main page.</h1>
|
||||||
|
<Link href="/">
|
||||||
|
<a>Go Home</a>
|
||||||
|
</Link>
|
||||||
|
</div> :
|
||||||
|
<div className={styles.overlay}>
|
||||||
|
{
|
||||||
|
this.state.showPlayerStats ? <PlayerStats
|
||||||
|
pp={data.pp.toLocaleString()}
|
||||||
|
globalPos={data.rank.toLocaleString()}
|
||||||
|
country={data.country}
|
||||||
|
countryRank={data.countryRank.toLocaleString()}
|
||||||
|
websiteType={websiteType}
|
||||||
|
avatar={data.profilePicture || data.avatar}
|
||||||
|
/>
|
||||||
|
: <></>
|
||||||
|
}
|
||||||
|
{
|
||||||
|
this.state.showScore && this.state.isVisible ?
|
||||||
|
<ScoreStats data={this.state} /> : <></>
|
||||||
|
}
|
||||||
|
{
|
||||||
|
this.state.showSongInfo && this.state.beatSaverData !== undefined && this.state.isVisible ?
|
||||||
|
<SongInfo data={this.state}/> : <></>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
@ -1,9 +1,11 @@
|
|||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
|
|
||||||
|
import styles from '../../styles/avatar.module.css';
|
||||||
|
|
||||||
const Avatar = (props) => {
|
const Avatar = (props) => {
|
||||||
return <>
|
return <>
|
||||||
<Image
|
<Image
|
||||||
className={'player-avatar'}
|
className={styles.playerAvatar}
|
||||||
src={props.url}
|
src={props.url}
|
||||||
width={180}
|
width={180}
|
||||||
height={180}
|
height={180}
|
||||||
|
13
src/components/Navbar.js
Normal file
13
src/components/Navbar.js
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { Navbar, Text } from "@nextui-org/react";
|
||||||
|
|
||||||
|
const NavBar = () => {
|
||||||
|
return <Navbar isBordered variant={"sticky"}>
|
||||||
|
<Navbar.Brand>
|
||||||
|
<Text b color="inherit">
|
||||||
|
BeatSaber Overlay
|
||||||
|
</Text>
|
||||||
|
</Navbar.Brand>
|
||||||
|
</Navbar>
|
||||||
|
}
|
||||||
|
|
||||||
|
export default NavBar;
|
@ -1,14 +1,22 @@
|
|||||||
import ReactCountryFlag from "react-country-flag";
|
import ReactCountryFlag from "react-country-flag";
|
||||||
|
|
||||||
|
import styles from '../../styles/playerStats.module.css';
|
||||||
|
import Avatar from "./Avatar";
|
||||||
|
|
||||||
const PlayerStats = (props) => {
|
const PlayerStats = (props) => {
|
||||||
return <div className={'player-stats'}>
|
return <div className={styles.playerStatsContainer}>
|
||||||
<p>{props.pp}pp <span style={{
|
<div>
|
||||||
fontSize: '20px',
|
<Avatar url={props.avatar} />
|
||||||
}}>({props.websiteType})</span></p>
|
</div>
|
||||||
<p>#{props.globalPos}</p>
|
<div className={styles.playerStats}>
|
||||||
<div className="player-country">
|
<p>{props.pp}pp <span style={{
|
||||||
<p>#{props.countryRank}</p>
|
fontSize: '20px',
|
||||||
<ReactCountryFlag className={'player-country-icon'} svg countryCode={props.country} />
|
}}>({props.websiteType})</span></p>
|
||||||
|
<p>#{props.globalPos}</p>
|
||||||
|
<div className={styles.playerCountry}>
|
||||||
|
<p>#{props.countryRank}</p>
|
||||||
|
<ReactCountryFlag className={styles.playerCountryIcon} svg countryCode={props.country} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
import {Component} from "react";
|
import {Component} from "react";
|
||||||
|
|
||||||
|
import styles from '../../styles/scoreStats.module.css';
|
||||||
|
|
||||||
export default class ScoreStats extends Component {
|
export default class ScoreStats extends Component {
|
||||||
|
|
||||||
constructor(params) {
|
constructor(params) {
|
||||||
@ -19,19 +21,19 @@ export default class ScoreStats extends Component {
|
|||||||
render() {
|
render() {
|
||||||
const data = this.props.data;
|
const data = this.props.data;
|
||||||
|
|
||||||
return <div className={'score-stats'}>
|
return <div className={styles.scoreStats}>
|
||||||
<div className={'score-stats-info'}>
|
<div className={styles.scoreStatsInfo}>
|
||||||
<p>{data.percentage}</p>
|
<p>{data.percentage}</p>
|
||||||
<p>{data.currentScore.toLocaleString()}</p>
|
<p>{data.currentScore.toLocaleString()}</p>
|
||||||
</div>
|
</div>
|
||||||
<p className={'score-stats-average-cut'}>Average Cut</p>
|
<p className={styles.scoreStatsAverageCut}>Average Cut</p>
|
||||||
<div className={'score-stats-hands'}>
|
<div className={styles.scoreStatsHands}>
|
||||||
<div className={'score-stats-left'}>
|
<div className={styles.scoreStatsLeft}>
|
||||||
<p>{this.getAverage(data.leftHand.averagePreSwing).toFixed(2)}</p>
|
<p>{this.getAverage(data.leftHand.averagePreSwing).toFixed(2)}</p>
|
||||||
<p>{this.getAverage(data.leftHand.averagePostSwing).toFixed(2)}</p>
|
<p>{this.getAverage(data.leftHand.averagePostSwing).toFixed(2)}</p>
|
||||||
<p>{this.getAverage(data.leftHand.averageCut).toFixed(2)}</p>
|
<p>{this.getAverage(data.leftHand.averageCut).toFixed(2)}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className={'score-stats-right'}>
|
<div className={styles.scoreStatsRight}>
|
||||||
<p>{this.getAverage(data.rightHand.averagePreSwing).toFixed(2)}</p>
|
<p>{this.getAverage(data.rightHand.averagePreSwing).toFixed(2)}</p>
|
||||||
<p>{this.getAverage(data.rightHand.averagePostSwing).toFixed(2)}</p>
|
<p>{this.getAverage(data.rightHand.averagePostSwing).toFixed(2)}</p>
|
||||||
<p>{this.getAverage(data.rightHand.averageCut).toFixed(2)}</p>
|
<p>{this.getAverage(data.rightHand.averageCut).toFixed(2)}</p>
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
import {Component} from "react";
|
import {Component} from "react";
|
||||||
|
|
||||||
|
import styles from '../../styles/songInfo.module.css';
|
||||||
|
|
||||||
export default class SongInfo extends Component {
|
export default class SongInfo extends Component {
|
||||||
|
|
||||||
constructor(params) {
|
constructor(params) {
|
||||||
@ -62,19 +64,19 @@ export default class SongInfo extends Component {
|
|||||||
// what in the fuck is this?? LMFAO
|
// what in the fuck is this?? LMFAO
|
||||||
const songTimerPercentage = ((this.props.data.currentSongTime / 1000) / (data.length / 1000)) * 100000;
|
const songTimerPercentage = ((this.props.data.currentSongTime / 1000) / (data.length / 1000)) * 100000;
|
||||||
|
|
||||||
return <div className={'song-info-container'}>
|
return <div className={styles.songInfoContainer}>
|
||||||
<img src={songArt}/>
|
<img src={songArt}/>
|
||||||
<div className={'song-info'}>
|
<div className={styles.songInfo}>
|
||||||
<p className={'song-info-song-name'}>{songName}</p>
|
<p className={styles.songInfoSongName}>{songName.length > 35 ? songName.substring(0, 35) + "..." : songName}</p>
|
||||||
<p className={'song-info-song-author'}>{songAuthorName}</p>
|
<p className={styles.songInfoSongAuthor}>{songAuthorName}</p>
|
||||||
<div className={'song-info-song-other-container'}>
|
<div className={styles.songInfoSongOtherContainer}>
|
||||||
<p className={'song-info-diff'} style={{ backgroundColor: this.state.diffColor }}>{difficulty}</p>
|
<p className={styles.songInfoDiff} style={{ backgroundColor: this.state.diffColor }}>{difficulty}</p>
|
||||||
<p className={'song-info-bsr'}>!bsr {bsr}</p>
|
<p className={styles.songInfoBsr}>!bsr {bsr}</p>
|
||||||
</div>
|
</div>
|
||||||
<p className={'song-time-text'}>{this.msToMinSeconds(this.props.data.currentSongTime * 1000)}/{this.msToMinSeconds(data.length)}</p>
|
<p className={styles.songTimeText}>{this.msToMinSeconds(this.props.data.currentSongTime * 1000)}/{this.msToMinSeconds(data.length)}</p>
|
||||||
<div className={'song-time-container'}>
|
<div className={styles.songTimeContainer}>
|
||||||
<div className={'song-time-background'}/>
|
<div className={styles.songTimeBackground}/>
|
||||||
<div className={'song-time'} style={{ width: songTimerPercentage + '%' }}/>
|
<div className={styles.songTime} style={{ width: songTimerPercentage + '%' }}/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
3
styles/avatar.module.css
Normal file
3
styles/avatar.module.css
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
.playerAvatar {
|
||||||
|
border-radius: 5%;
|
||||||
|
}
|
@ -1,196 +1,3 @@
|
|||||||
* {
|
*, html, body {
|
||||||
font-family: 'Roboto', sans-serif !important;
|
|
||||||
color: white !important;
|
|
||||||
font-size: xx-large;
|
|
||||||
line-height: 1.4em !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Needed to see the UI in the browser,
|
|
||||||
* when using OBS it overrides the background color anyway
|
|
||||||
*/
|
|
||||||
body, html {
|
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.loading {
|
|
||||||
color: black;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info {
|
|
||||||
margin-top: 35px;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info a {
|
|
||||||
font-size: large;
|
|
||||||
text-decoration: none;
|
|
||||||
color: #7299d4;
|
|
||||||
transition: 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info a:hover {
|
|
||||||
color: #4b72d3 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.overlay {
|
|
||||||
padding: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.invalid-player p {
|
|
||||||
font-size: large !important;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.invalid-player b {
|
|
||||||
font-size: large !important;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.player-stats-container {
|
|
||||||
display: flex;
|
|
||||||
margin-left: -5px;
|
|
||||||
margin-top: -5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.player-country {
|
|
||||||
display: flex;
|
|
||||||
line-height: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
.player-country span {
|
|
||||||
margin-top: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.player-country p {
|
|
||||||
margin: 0 6px 0 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.player-country-icon {
|
|
||||||
margin-top: 7px;
|
|
||||||
border-radius: 30%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.player-avatar {
|
|
||||||
border-radius: 5%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.player-stats {
|
|
||||||
display: block;
|
|
||||||
margin-left: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.player-stats p {
|
|
||||||
font-size: xx-large;
|
|
||||||
}
|
|
||||||
|
|
||||||
.score-stats {
|
|
||||||
text-align: center;
|
|
||||||
position:absolute;
|
|
||||||
top:0;
|
|
||||||
right:0;
|
|
||||||
margin-right: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.score-stats p {
|
|
||||||
font-size: xx-large;
|
|
||||||
}
|
|
||||||
|
|
||||||
.score-stats-right {
|
|
||||||
margin-right: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.score-stats-average-cut {
|
|
||||||
font-weight: bold;
|
|
||||||
font-size: x-large;
|
|
||||||
}
|
|
||||||
|
|
||||||
.score-stats-hands {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.score-stats-hands div {
|
|
||||||
padding-left: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.score-stats-info {
|
|
||||||
margin-right: 10px;
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
.song-info-container {
|
|
||||||
display: flex;
|
|
||||||
position: fixed;
|
|
||||||
bottom:0;
|
|
||||||
left:0;
|
|
||||||
margin-left: 5px;
|
|
||||||
margin-bottom: 5px;
|
|
||||||
font-size: large !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.song-info-container img {
|
|
||||||
border-radius: 5%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.song-info {
|
|
||||||
margin-left: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.song-info p {
|
|
||||||
font-size: x-large;
|
|
||||||
}
|
|
||||||
|
|
||||||
.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: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.song-info-diff {
|
|
||||||
font-size: large;
|
|
||||||
padding: 0px 4px;
|
|
||||||
width: fit-content;
|
|
||||||
border-radius: 5%;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.song-info-bsr {
|
|
||||||
font-size: large;
|
|
||||||
margin-left: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.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 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.song-time-text {
|
|
||||||
margin-top: 6px;
|
|
||||||
font-size: large;
|
|
||||||
}
|
|
3
styles/main.module.css
Normal file
3
styles/main.module.css
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
.main {
|
||||||
|
font-family: 'Roboto', sans-serif !important;
|
||||||
|
}
|
10
styles/overlay.module.css
Normal file
10
styles/overlay.module.css
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
.main {
|
||||||
|
font-family: 'Roboto', sans-serif !important;
|
||||||
|
color: white;
|
||||||
|
font-size: xx-large;
|
||||||
|
line-height: 1.4em !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading {
|
||||||
|
color: black;
|
||||||
|
}
|
41
styles/playerStats.module.css
Normal file
41
styles/playerStats.module.css
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
.playerStatsContainer {
|
||||||
|
display: flex;
|
||||||
|
margin-left: 5px;
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.playerStatsContainer p {
|
||||||
|
font-size: xx-large;
|
||||||
|
line-height: 1.4em !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.playerCountry {
|
||||||
|
display: flex;
|
||||||
|
line-height: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.playerCountry span {
|
||||||
|
margin-top: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.playerCountry p {
|
||||||
|
margin: 0 6px 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.playerCountryIcon {
|
||||||
|
margin-top: 7px;
|
||||||
|
border-radius: 30%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.playerAvatar {
|
||||||
|
border-radius: 5%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.playerStats {
|
||||||
|
display: block;
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.playerStats p {
|
||||||
|
font-size: xx-large;
|
||||||
|
}
|
34
styles/scoreStats.module.css
Normal file
34
styles/scoreStats.module.css
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
.scoreStats {
|
||||||
|
text-align: center;
|
||||||
|
position:absolute;
|
||||||
|
top:0;
|
||||||
|
right:0;
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scoreStats p {
|
||||||
|
font-size: xx-large;
|
||||||
|
line-height: 1.4em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scoreStatsRight {
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scoreStatsAverageCut {
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: x-large;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scoreStatsHands {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scoreStatsHands div {
|
||||||
|
padding-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scoreStatsInfo {
|
||||||
|
margin-right: 10px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
74
styles/songInfo.module.css
Normal file
74
styles/songInfo.module.css
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
.songInfoContainer {
|
||||||
|
display: flex;
|
||||||
|
position: fixed;
|
||||||
|
bottom:0;
|
||||||
|
left:0;
|
||||||
|
margin-left: 5px;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
font-size: large !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.songInfoContainer img {
|
||||||
|
border-radius: 5%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.songInfo {
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.songInfoSongName {
|
||||||
|
font-size: x-large;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.songInfoSongAuthor {
|
||||||
|
font-size: large;
|
||||||
|
margin-top: -5px;
|
||||||
|
margin-bottom: -5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.songInfoSongOtherContainer {
|
||||||
|
display: flex;
|
||||||
|
margin-top: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.songInfoDiff {
|
||||||
|
font-size: large;
|
||||||
|
padding: 0px 4px;
|
||||||
|
width: fit-content;
|
||||||
|
border-radius: 5%;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.songInfoBsr {
|
||||||
|
font-size: large;
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.songTimeContainer {
|
||||||
|
width: 200px;
|
||||||
|
display: grid;
|
||||||
|
}
|
||||||
|
|
||||||
|
.songTimeBackground {
|
||||||
|
grid-column: 1;
|
||||||
|
grid-row: 1;
|
||||||
|
z-index: 1;
|
||||||
|
padding: 3px;
|
||||||
|
width: 100%;
|
||||||
|
background-color: white !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.songTime {
|
||||||
|
grid-column: 1;
|
||||||
|
grid-row: 1;
|
||||||
|
z-index: 2;
|
||||||
|
padding: 3px;
|
||||||
|
background-color: red !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.songTimeText {
|
||||||
|
margin-top: 6px;
|
||||||
|
font-size: large;
|
||||||
|
}
|
Reference in New Issue
Block a user