Add settings UI, song time, accuracy, mistakes
This commit is contained in:
parent
0d22d036f7
commit
e3b376b9a8
39
README.md
39
README.md
@ -2,28 +2,29 @@
|
|||||||
|
|
||||||
Simple Beat Saber stream overlay for [twitch.tv/iza_k](https://www.twitch.tv/iza_k)
|
Simple Beat Saber stream overlay for [twitch.tv/iza_k](https://www.twitch.tv/iza_k)
|
||||||
|
|
||||||
Requires [BeatSaberPlus](https://github.com/hardcpp/BeatSaberPlus) SongOverlay
|
Requires [BeatSaberPlus](https://github.com/hardcpp/BeatSaberPlus)
|
||||||
|
|
||||||
### Preview
|
### Preview
|
||||||
<img width="606" height="96" alt="Screenshot" src="https://github.com/user-attachments/assets/96b8e7ce-66ed-4327-a915-ee309d2296c3" />
|
|
||||||
|
|
||||||
## Usage - OBS Studio
|

|
||||||
Add Source > Browser > URL: `https://bs-overlay.netlify.app/`
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
1. Go to [bs-overlay.netlify.app](https://bs-overlay.netlify.app/)
|
||||||
|
2. Click anywhere on page to show settings
|
||||||
|

|
||||||
|
3. Copy URL
|
||||||
|
4. OBS Studio: Add Source > Browser > paste URL
|
||||||
|
5. BeatSaber+ settings, enable the Song Overlay module
|
||||||
|
|
||||||
|
### Advanced
|
||||||
|
|
||||||
|
[Download](https://github.com/ibillingsley/BeatSaber-Overlay/archive/refs/heads/main.zip) the source code to use the overlay locally without hosting it online.
|
||||||
|
|
||||||
|
Note: in OBS browser source, use URL `file:///C:/path-to-overlay.../index.html` instead of "Local file" so that URL parameters work.
|
||||||
|
|
||||||
|
You can further customize the overlay with Custom CSS. Example:
|
||||||
|
|
||||||
### Configuration - Custom CSS
|
|
||||||
To change size, set root font size (default 10px)
|
|
||||||
```css
|
```css
|
||||||
:root { font-size: 15px; }
|
body { font-family: "Comic Sans MS"; }
|
||||||
```
|
|
||||||
To right align
|
|
||||||
```css
|
|
||||||
body > .row { flex-direction: row-reverse; } .row { justify-content: flex-end; }
|
|
||||||
```
|
|
||||||
To change fade duration (default 300ms)
|
|
||||||
```css
|
|
||||||
body { transition-duration: 500ms; }
|
|
||||||
```
|
|
||||||
To change font
|
|
||||||
```css
|
|
||||||
body { font-family: "Comic Sans MS", cursive; }
|
|
||||||
```
|
```
|
||||||
|
|||||||
BIN
fonts/montserrat-600.woff2
Normal file
BIN
fonts/montserrat-600.woff2
Normal file
Binary file not shown.
BIN
fonts/montserrat-700.woff2
Normal file
BIN
fonts/montserrat-700.woff2
Normal file
Binary file not shown.
Binary file not shown.
BIN
images/screenshots/preview.png
Normal file
BIN
images/screenshots/preview.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 30 KiB |
BIN
images/screenshots/settings.png
Normal file
BIN
images/screenshots/settings.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
187
index.css
187
index.css
@ -1,27 +1,44 @@
|
|||||||
@font-face {
|
@font-face {
|
||||||
font-family: "Montserrat";
|
font-family: "Montserrat";
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
font-weight: 600 700;
|
font-weight: 600;
|
||||||
src: url("fonts/montserrat.woff2") format("woff2");
|
src: url("fonts/montserrat-600.woff2") format("woff2");
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "Montserrat";
|
||||||
|
font-display: swap;
|
||||||
|
font-weight: 700;
|
||||||
|
src: url("fonts/montserrat-700.woff2") format("woff2");
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
html {
|
html {
|
||||||
font-size: 10px;
|
font-size: calc(var(--scale, 1) * 10px);
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
height: 100vh;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 1.4rem 1.6rem;
|
padding: 1.4rem 1.6rem;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
font-family: "Montserrat", sans-serif;
|
font-family: "Montserrat", sans-serif;
|
||||||
font-size: 1.5rem;
|
font-size: 1.6rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
line-height: 1.2;
|
line-height: 1.2;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
background: rgba(0, 0, 0, 0);
|
background: rgba(0, 0, 0, 0);
|
||||||
color: white;
|
color: white;
|
||||||
filter: drop-shadow(0 0 0.1rem black) drop-shadow(0 0 0.1rem black);
|
filter: drop-shadow(0 0 0.1rem black) drop-shadow(0 0 0.1rem black);
|
||||||
transition: opacity 300ms ease-out;
|
transition-property: opacity;
|
||||||
|
transition-timing-function: ease-out;
|
||||||
|
transition-duration: calc(var(--fade, 300) * 1ms);
|
||||||
}
|
}
|
||||||
|
|
||||||
body.loading,
|
body.loading,
|
||||||
@ -29,12 +46,27 @@ body:not([data-game-state="Playing"], .preview) {
|
|||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
span:empty {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
.row {
|
.row {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 1.6rem;
|
gap: 1.6rem;
|
||||||
|
align-items: baseline;
|
||||||
}
|
}
|
||||||
|
|
||||||
.column {
|
/* Map info */
|
||||||
|
|
||||||
|
#coverImg {
|
||||||
|
width: 6.8rem;
|
||||||
|
height: 6.8rem;
|
||||||
|
border-radius: 0.6rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
align-self: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mapInfo {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
@ -43,25 +75,13 @@ body:not([data-game-state="Playing"], .preview) {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.column * {
|
#mapInfo .row {
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
|
||||||
|
|
||||||
.column .row {
|
|
||||||
align-items: baseline;
|
|
||||||
gap: 0.9rem;
|
gap: 0.9rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.column span:empty {
|
#mapInfo * {
|
||||||
display: none;
|
overflow: hidden;
|
||||||
}
|
text-overflow: ellipsis;
|
||||||
|
|
||||||
#coverImg {
|
|
||||||
width: 6.8rem;
|
|
||||||
height: 6.8rem;
|
|
||||||
border-radius: 0.6rem;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#title {
|
#title {
|
||||||
@ -69,19 +89,13 @@ body:not([data-game-state="Playing"], .preview) {
|
|||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
#subTitle,
|
#mapper::before,
|
||||||
#artist,
|
#difficultyLabel::before {
|
||||||
#mapper {
|
|
||||||
font-size: 1.6rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
#mapper:before,
|
|
||||||
#difficultyLabel:before {
|
|
||||||
content: "‹";
|
content: "‹";
|
||||||
}
|
}
|
||||||
|
|
||||||
#mapper:after,
|
#mapper::after,
|
||||||
#difficultyLabel:after {
|
#difficultyLabel::after {
|
||||||
content: "›";
|
content: "›";
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -90,16 +104,20 @@ body:not([data-game-state="Playing"], .preview) {
|
|||||||
flex-shrink: 99999;
|
flex-shrink: 99999;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#difficulty,
|
||||||
|
#difficultyLabel {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
#characteristicImg {
|
#characteristicImg {
|
||||||
width: 1.1em;
|
width: 1.7rem;
|
||||||
height: 1.1em;
|
height: 1.7rem;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
align-self: center;
|
align-self: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
#type,
|
#type,
|
||||||
#bsrKey {
|
#bsrKey {
|
||||||
font-size: 1.6rem;
|
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
@ -108,7 +126,7 @@ body:not([data-game-state="Playing"], .preview) {
|
|||||||
letter-spacing: 0.2rem;
|
letter-spacing: 0.2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
#bsrKey:before {
|
#bsrKey::before {
|
||||||
content: "!bsr ";
|
content: "!bsr ";
|
||||||
letter-spacing: normal;
|
letter-spacing: normal;
|
||||||
}
|
}
|
||||||
@ -116,3 +134,100 @@ body:not([data-game-state="Playing"], .preview) {
|
|||||||
#type:not(:empty) + #bsrKey {
|
#type:not(:empty) + #bsrKey {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Song time */
|
||||||
|
|
||||||
|
#time {
|
||||||
|
align-self: flex-start;
|
||||||
|
width: 6.8rem;
|
||||||
|
font-size: 1.35rem;
|
||||||
|
text-align: right;
|
||||||
|
line-height: 1.4;
|
||||||
|
letter-spacing: -0.05em;
|
||||||
|
}
|
||||||
|
|
||||||
|
#time,
|
||||||
|
#accuracy,
|
||||||
|
#mistakes {
|
||||||
|
font-feature-settings: "tnum";
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Score */
|
||||||
|
|
||||||
|
#score {
|
||||||
|
display: contents;
|
||||||
|
}
|
||||||
|
|
||||||
|
#accuracy {
|
||||||
|
font-size: 2.4rem;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
#accuracy::after {
|
||||||
|
display: inline-block;
|
||||||
|
content: "%";
|
||||||
|
font-size: 1.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#accuracy.failed {
|
||||||
|
text-decoration: line-through red;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mistakes {
|
||||||
|
display: inline;
|
||||||
|
font-size: 1.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mistakes::after {
|
||||||
|
content: "⤫";
|
||||||
|
}
|
||||||
|
|
||||||
|
#mistakes:empty::after {
|
||||||
|
content: "FC";
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Settings */
|
||||||
|
|
||||||
|
body:not(.cover) #coverImg,
|
||||||
|
body:not(.mapInfo) #mapInfo,
|
||||||
|
body:not(.time) #time,
|
||||||
|
body:not(.score) #score {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.right > .row {
|
||||||
|
flex-direction: row-reverse;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.right #mapInfo .row {
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.bottom {
|
||||||
|
flex-direction: column-reverse;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.bottom #time {
|
||||||
|
align-self: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
#settings {
|
||||||
|
display: none;
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
font-family: system-ui;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview #settings {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
#settings label * {
|
||||||
|
float: right;
|
||||||
|
margin-left: 1em;
|
||||||
|
}
|
||||||
|
|||||||
27
index.html
27
index.html
@ -8,7 +8,7 @@
|
|||||||
<body>
|
<body>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<img id="coverImg" src="images/unknown.svg">
|
<img id="coverImg" src="images/unknown.svg">
|
||||||
<div class="column">
|
<div id="mapInfo">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<span id="title">Title</span>
|
<span id="title">Title</span>
|
||||||
<span id="subTitle">Subtitle</span>
|
<span id="subTitle">Subtitle</span>
|
||||||
@ -26,6 +26,31 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div id="time">1:23 / 4:56</div>
|
||||||
|
<div id="score">
|
||||||
|
<span id="accuracy">96.9</span>
|
||||||
|
<span id="mistakes">7</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<dialog id="settings">
|
||||||
|
<strong>Settings</strong>
|
||||||
|
<label>Show cover: <input id="coverInput" type="checkbox"></label>
|
||||||
|
<label>Show map info: <input id="mapInfoInput" type="checkbox"></label>
|
||||||
|
<label>Show time: <input id="timeInput" type="checkbox"></label>
|
||||||
|
<label>Show score: <input id="scoreInput" type="checkbox"></label>
|
||||||
|
<label>Position: <select id="positionInput">
|
||||||
|
<option value="[false,false]">Top left</option>
|
||||||
|
<option value="[true,false]">Top right</option>
|
||||||
|
<option value="[false,true]">Bottom left</option>
|
||||||
|
<option value="[true,true]">Bottom right</option>
|
||||||
|
</select></label>
|
||||||
|
<label>Scale (%): <input id="scaleInput" type="number" min="10" max="1000" step="5"></label>
|
||||||
|
<label>Fade (ms): <input id="fadeInput" type="number" min="0" max="5000" step="10"></label>
|
||||||
|
<br>
|
||||||
|
<strong>About</strong>
|
||||||
|
<a href="https://github.com/ibillingsley/BeatSaber-Overlay" target="_blank">Source code</a>
|
||||||
|
</dialog>
|
||||||
<script src="index.js"></script>
|
<script src="index.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
139
index.js
139
index.js
@ -1,6 +1,6 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
// Data providers
|
// WebSocket connection
|
||||||
|
|
||||||
const beatSaberPlus = {
|
const beatSaberPlus = {
|
||||||
// https://github.com/hardcpp/BeatSaberPlus/wiki/%5BEN%5D-Song-Overlay
|
// https://github.com/hardcpp/BeatSaberPlus/wiki/%5BEN%5D-Song-Overlay
|
||||||
@ -20,6 +20,18 @@ const beatSaberPlus = {
|
|||||||
case "mapInfo":
|
case "mapInfo":
|
||||||
updateMapInfo(data.mapInfoChanged);
|
updateMapInfo(data.mapInfoChanged);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case "pause":
|
||||||
|
updateTime(data.pauseTime, true);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "resume":
|
||||||
|
updateTime(data.resumeTime, false);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "score":
|
||||||
|
updateScore(data.scoreEvent);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@ -30,8 +42,6 @@ const beatSaberPlus = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// WebSocket connection
|
|
||||||
|
|
||||||
const provider = beatSaberPlus;
|
const provider = beatSaberPlus;
|
||||||
const retryMs = 10000;
|
const retryMs = 10000;
|
||||||
let retries = 0;
|
let retries = 0;
|
||||||
@ -55,6 +65,8 @@ function onClose(e) {
|
|||||||
setTimeout(connect, retryMs);
|
setTimeout(connect, retryMs);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
connect();
|
||||||
|
|
||||||
// Map info
|
// Map info
|
||||||
|
|
||||||
const cover = document.getElementById("coverImg");
|
const cover = document.getElementById("coverImg");
|
||||||
@ -67,12 +79,14 @@ const characteristic = document.getElementById("characteristicImg");
|
|||||||
const difficultyLabel = document.getElementById("difficultyLabel");
|
const difficultyLabel = document.getElementById("difficultyLabel");
|
||||||
const type = document.getElementById("type");
|
const type = document.getElementById("type");
|
||||||
const bsrKey = document.getElementById("bsrKey");
|
const bsrKey = document.getElementById("bsrKey");
|
||||||
|
let timeMultiplier = 1;
|
||||||
|
let duration = 0;
|
||||||
|
|
||||||
/** @param {MapInfoChanged} data */
|
/** @param {MapInfoChanged} data */
|
||||||
async function updateMapInfo(data) {
|
async function updateMapInfo(data) {
|
||||||
const custom = data.level_id.startsWith("custom_level_");
|
const custom = data.level_id.startsWith("custom_level_");
|
||||||
const wip = custom && data.level_id.endsWith("WIP");
|
const wip = custom && data.level_id.endsWith("WIP");
|
||||||
cover.src = data.coverRaw ? `data:image/jpeg;base64,${data.coverRaw}` : "images/unknown.svg";
|
cover.src = data.coverRaw ? `data:image/png;base64,${data.coverRaw}` : "images/unknown.svg";
|
||||||
title.textContent = data.name || "";
|
title.textContent = data.name || "";
|
||||||
subTitle.textContent = data.sub_name || "";
|
subTitle.textContent = data.sub_name || "";
|
||||||
artist.textContent = data.artist || "";
|
artist.textContent = data.artist || "";
|
||||||
@ -82,6 +96,8 @@ async function updateMapInfo(data) {
|
|||||||
difficultyLabel.textContent = ""; // BS+ does not provide label
|
difficultyLabel.textContent = ""; // BS+ does not provide label
|
||||||
type.textContent = !custom ? "OST" : wip ? "WIP" : "";
|
type.textContent = !custom ? "OST" : wip ? "WIP" : "";
|
||||||
bsrKey.textContent = data.BSRKey || "???"; // Always empty?
|
bsrKey.textContent = data.BSRKey || "???"; // Always empty?
|
||||||
|
timeMultiplier = data.timeMultiplier || 1;
|
||||||
|
duration = data.duration / 1000;
|
||||||
|
|
||||||
// Fetch extra info from BeatSaver
|
// Fetch extra info from BeatSaver
|
||||||
if (custom && !wip) {
|
if (custom && !wip) {
|
||||||
@ -103,6 +119,117 @@ async function updateMapInfo(data) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
connect();
|
// Song time
|
||||||
|
|
||||||
document.documentElement.onclick = () => document.body.classList.toggle("preview");
|
const timeText = document.getElementById("time");
|
||||||
|
const intervalMs = 500;
|
||||||
|
let intervalId = 0;
|
||||||
|
let currentTime = 0;
|
||||||
|
|
||||||
|
/** @param {number} time @param {boolean} paused */
|
||||||
|
function updateTime(time, paused) {
|
||||||
|
if (!settings.time) return;
|
||||||
|
currentTime = time;
|
||||||
|
timeText.textContent = `${formatTime(currentTime)} / ${formatTime(duration)}`;
|
||||||
|
clearInterval(intervalId);
|
||||||
|
if (paused) return;
|
||||||
|
intervalId = window.setInterval(() => {
|
||||||
|
currentTime += intervalMs * timeMultiplier / 1000;
|
||||||
|
timeText.textContent = `${formatTime(currentTime)} / ${formatTime(duration)}`;
|
||||||
|
}, intervalMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param {number} t */
|
||||||
|
function formatTime(t) {
|
||||||
|
t = Math.floor(t);
|
||||||
|
const minutes = Math.floor(t / 60);
|
||||||
|
const seconds = t - minutes * 60;
|
||||||
|
return `${minutes}:${String(seconds).padStart(2, "0")}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Score
|
||||||
|
|
||||||
|
const accuracy = document.getElementById("accuracy");
|
||||||
|
const mistakes = document.getElementById("mistakes");
|
||||||
|
|
||||||
|
/** @param {Score} score */
|
||||||
|
function updateScore(score) {
|
||||||
|
if (!settings.score) return;
|
||||||
|
accuracy.textContent = (score.accuracy * 100).toFixed(1);
|
||||||
|
mistakes.textContent = score.missCount ? String(score.missCount) : "";
|
||||||
|
accuracy.classList.toggle("failed", score.currentHealth === 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Settings
|
||||||
|
|
||||||
|
const settings = {
|
||||||
|
cover: true,
|
||||||
|
mapInfo: true,
|
||||||
|
time: true,
|
||||||
|
score: true,
|
||||||
|
right: false,
|
||||||
|
bottom: false,
|
||||||
|
scale: 1,
|
||||||
|
fade: 300,
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaults = structuredClone(settings);
|
||||||
|
const style = document.createElement("style");
|
||||||
|
|
||||||
|
function loadSettings() {
|
||||||
|
const params = new URLSearchParams(location.hash.slice(1));
|
||||||
|
let css = "";
|
||||||
|
for (const key of Object.keys(settings)) {
|
||||||
|
const value = JSON.parse(params.get(key) || "null") ?? defaults[key];
|
||||||
|
settings[key] = value;
|
||||||
|
if (typeof value === "boolean") document.body.classList.toggle(key, value);
|
||||||
|
else css += `--${key}: ${value}; `;
|
||||||
|
}
|
||||||
|
style.textContent = `:root { ${css}}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveSettings() {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
for (const [key, value] of Object.entries(settings))
|
||||||
|
if (value !== defaults[key]) params.set(key, JSON.stringify(value));
|
||||||
|
location.replace("#" + params.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
window.onhashchange = loadSettings;
|
||||||
|
loadSettings();
|
||||||
|
document.head.appendChild(style);
|
||||||
|
|
||||||
|
// Settings UI
|
||||||
|
|
||||||
|
for (const key of ["cover", "mapInfo", "time", "score"]) {
|
||||||
|
const input = document.getElementById(`${key}Input`);
|
||||||
|
input.checked = settings[key];
|
||||||
|
input.oninput = () => {
|
||||||
|
settings[key] = input.checked;
|
||||||
|
saveSettings();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const scale = document.getElementById("scaleInput");
|
||||||
|
scale.valueAsNumber = settings.scale * 100;
|
||||||
|
scale.oninput = () => {
|
||||||
|
settings.scale = scale.valueAsNumber / 100;
|
||||||
|
saveSettings();
|
||||||
|
};
|
||||||
|
|
||||||
|
const position = document.getElementById("positionInput");
|
||||||
|
position.value = JSON.stringify([settings.right, settings.bottom]);
|
||||||
|
position.onchange = () => {
|
||||||
|
[settings.right, settings.bottom] = JSON.parse(position.value);
|
||||||
|
saveSettings();
|
||||||
|
};
|
||||||
|
|
||||||
|
const fade = document.getElementById("fadeInput");
|
||||||
|
fade.valueAsNumber = settings.fade;
|
||||||
|
fade.oninput = () => {
|
||||||
|
settings.fade = fade.valueAsNumber;
|
||||||
|
saveSettings();
|
||||||
|
};
|
||||||
|
|
||||||
|
document.documentElement.onclick = e => document.body.classList.toggle("preview");
|
||||||
|
document.getElementById("settings").onclick = e => e.stopPropagation();
|
||||||
|
|||||||
1
types.d.ts
vendored
1
types.d.ts
vendored
@ -62,6 +62,7 @@ interface ScoreEvent {
|
|||||||
|
|
||||||
type BeatSaberPlusEvent = HandshakeEvent | GameStateEvent | ResumeEvent | PauseEvent | MapInfoChangedEvent | ScoreEvent;
|
type BeatSaberPlusEvent = HandshakeEvent | GameStateEvent | ResumeEvent | PauseEvent | MapInfoChangedEvent | ScoreEvent;
|
||||||
type MapInfoChanged = MapInfoChangedEvent["mapInfoChanged"];
|
type MapInfoChanged = MapInfoChangedEvent["mapInfoChanged"];
|
||||||
|
type Score = ScoreEvent["scoreEvent"];
|
||||||
|
|
||||||
interface Document {
|
interface Document {
|
||||||
// Assume non-null
|
// Assume non-null
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user