Add files via upload
@@ -0,0 +1,47 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="shortcut icon" href="#" />
|
||||
<link rel="stylesheet" href="./style.css" />
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@xz/fonts@1/serve/metropolis.min.css" />
|
||||
<script src="https://cdn.socket.io/4.7.5/socket.io.min.js"
|
||||
integrity="sha384-2huaZvOR9iDzHqslqwpR87isEmrfxqyWOF7hr7BY6KG0+hVKLoEXMPUJw3ynWuhO"
|
||||
crossorigin="anonymous"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="statusContainer">Connecting...</div>
|
||||
<div id="mainContainer">
|
||||
<!-- <div id="albumArtBox">
|
||||
<img id="albumArt"
|
||||
src="https://static.wikia.nocookie.net/dreamcatcherwiki/images/d/dc/Dystopia_Lose_Myself_Digital_Cover.jpg"></img>
|
||||
<img id="albumArtBack"
|
||||
src="https://cdns-images.dzcdn.net/images/cover/762f7ef8a0c20e91304a3fae47449ccd/1900x1900-000000-80-0-0.jpg"></img>
|
||||
</div> -->
|
||||
<div id="songInfoBox">
|
||||
<div id="songInfo">
|
||||
<div id="IAmRunningOutOfNamesForTheseBoxes">
|
||||
<div id="songLabel">Can't get you out of my mind</div>
|
||||
<div id="artistLabel">Dreamcatcher</div>
|
||||
<!-- <div id="times">
|
||||
<div id="progressTime">2:29</div>
|
||||
<div id="duration">3:43</div>
|
||||
</div>
|
||||
<div id="progressBg">
|
||||
<div id="progressBar"></div>
|
||||
</div> -->
|
||||
</div>
|
||||
</div>
|
||||
<div id="backgroundArt">
|
||||
<img id="backgroundImage"
|
||||
src="https://static.wikia.nocookie.net/dreamcatcherwiki/images/d/dc/Dystopia_Lose_Myself_Digital_Cover.jpg"></img>
|
||||
<img id="backgroundImageBack"
|
||||
src="https://cdns-images.dzcdn.net/images/cover/762f7ef8a0c20e91304a3fae47449ccd/1900x1900-000000-80-0-0.jpg"></img>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
<script src='https://cdnjs.cloudflare.com/ajax/libs/gsap/1.18.0/TweenMax.min.js'></script>
|
||||
<script src="./script.js"></script>
|
||||
@@ -0,0 +1,230 @@
|
||||
///////////////
|
||||
// PARAMETRS //
|
||||
///////////////
|
||||
|
||||
const queryString = window.location.search;
|
||||
const urlParams = new URLSearchParams(queryString);
|
||||
|
||||
const visibilityDuration = urlParams.get("duration") || 0;
|
||||
// const hideAlbumArt = urlParams.has("hideAlbumArt");
|
||||
|
||||
|
||||
/////////////////
|
||||
// GLOBAL VARS //
|
||||
/////////////////
|
||||
|
||||
let animationSpeed = 0.5;
|
||||
|
||||
|
||||
|
||||
//////////////////////
|
||||
// WEBSOCKET SERVER //
|
||||
//////////////////////
|
||||
|
||||
// This is the main function that connects to the Streamer.bot websocket server
|
||||
function connectws() {
|
||||
if ("WebSocket" in window) {
|
||||
//const ws = new WebSocket("ws://" + sbServerAddress + ":" + sbServerPort + "/");
|
||||
const CiderApp = io("http://localhost:10767/", {
|
||||
transports: ['websocket']
|
||||
});
|
||||
|
||||
CiderApp.on("disconnect", (event) => {
|
||||
SetConnectionStatus(false);
|
||||
setTimeout(connectws, 5000);
|
||||
});
|
||||
|
||||
CiderApp.on("connect", (event) => {
|
||||
SetConnectionStatus(true);
|
||||
});
|
||||
|
||||
// Set up websocket artwork/information handling
|
||||
CiderApp.on("API:Playback", ({ data, type }) => {
|
||||
switch (type) {
|
||||
// Song changes
|
||||
case ("playbackStatus.nowPlayingItemDidChange"):
|
||||
UpdateSongInfo(data);
|
||||
break;
|
||||
|
||||
// Progress bar moves
|
||||
case ("playbackStatus.playbackTimeDidChange"):
|
||||
UpdateProgressBar(data);
|
||||
break;
|
||||
|
||||
// Pause/unpause
|
||||
case ("playbackStatus.playbackStateDidChange"):
|
||||
UpdatePlaybackState(data);
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
////////////////////////
|
||||
// NOW PLAYING WIDGET //
|
||||
////////////////////////
|
||||
|
||||
function UpdateSongInfo(data) {
|
||||
// Set the user's info
|
||||
let albumArtUrl = data.artwork.url;
|
||||
albumArtUrl = albumArtUrl.replace("{w}", data.artwork.width);
|
||||
albumArtUrl = albumArtUrl.replace("{h}", data.artwork.height);
|
||||
|
||||
// UpdateAlbumArt(document.getElementById("albumArt"), albumArtUrl);
|
||||
UpdateAlbumArt(document.getElementById("backgroundImage"), albumArtUrl);
|
||||
|
||||
setTimeout(() => {
|
||||
UpdateTextLabel(document.getElementById("songLabel"), data.name);
|
||||
UpdateTextLabel(document.getElementById("artistLabel"), data.artistName);
|
||||
}, animationSpeed * 500);
|
||||
|
||||
setTimeout(() => {
|
||||
// document.getElementById("albumArtBack").src = albumArtUrl;
|
||||
document.getElementById("backgroundImageBack").src = albumArtUrl;
|
||||
}, 2 * animationSpeed * 500);
|
||||
|
||||
if (visibilityDuration > 0) {
|
||||
setTimeout(() => {
|
||||
SetVisibility(false);
|
||||
}, visibilityDuration * 1000);
|
||||
}
|
||||
}
|
||||
|
||||
function UpdateTextLabel(div, text) {
|
||||
if (div.innerHTML != text) {
|
||||
div.setAttribute("class", "text-fade");
|
||||
setTimeout(() => {
|
||||
div.innerHTML = text;
|
||||
div.setAttribute("class", ".text-show");
|
||||
}, animationSpeed * 250);
|
||||
}
|
||||
}
|
||||
|
||||
function UpdateAlbumArt(div, imgsrc) {
|
||||
if (div.src != imgsrc) {
|
||||
div.setAttribute("class", "text-fade");
|
||||
setTimeout(() => {
|
||||
div.src = imgsrc;
|
||||
div.setAttribute("class", "text-show");
|
||||
}, animationSpeed * 500);
|
||||
}
|
||||
}
|
||||
|
||||
function UpdateProgressBar(data) {
|
||||
const progress = ((data.currentPlaybackTime / data.currentPlaybackDuration) * 100);
|
||||
const progressTime = ConvertSecondsToMinutesSoThatItLooksBetterOnTheOverlay(data.currentPlaybackTime);
|
||||
const duration = ConvertSecondsToMinutesSoThatItLooksBetterOnTheOverlay(data.currentPlaybackTimeRemaining);
|
||||
// document.getElementById("progressBar").style.width = `${progress}%`;
|
||||
// document.getElementById("progressTime").innerHTML = progressTime;
|
||||
// document.getElementById("duration").innerHTML = `-${duration}`;
|
||||
document.getElementById("backgroundImage").style.clipPath = `inset(0 ${100 - progress}% 0 0)`;
|
||||
}
|
||||
|
||||
function UpdatePlaybackState(data) {
|
||||
console.log(data);
|
||||
switch (data.state) {
|
||||
case ("paused"):
|
||||
case ("stopped"):
|
||||
SetVisibility(false);
|
||||
break;
|
||||
case ("playing"):
|
||||
UpdateSongInfo(data.attributes);
|
||||
setTimeout(() => {
|
||||
SetVisibility(true);
|
||||
}, animationSpeed * 500);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
//////////////////////
|
||||
// HELPER FUNCTIONS //
|
||||
//////////////////////
|
||||
|
||||
function ConvertSecondsToMinutesSoThatItLooksBetterOnTheOverlay(time) {
|
||||
const minutes = Math.floor(time / 60);
|
||||
const seconds = Math.trunc(time - minutes * 60);
|
||||
|
||||
return `${minutes}:${('0' + seconds).slice(-2)}`;
|
||||
}
|
||||
|
||||
function SetVisibility(isVisible) {
|
||||
widgetVisibility = isVisible;
|
||||
|
||||
const mainContainer = document.getElementById("mainContainer");
|
||||
|
||||
if (isVisible) {
|
||||
var tl = new TimelineMax();
|
||||
tl
|
||||
.to(mainContainer, animationSpeed, { bottom: "50%", ease: Power1.easeInOut }, 'label')
|
||||
.to(mainContainer, animationSpeed, { opacity: 1, ease: Power1.easeInOut }, 'label')
|
||||
}
|
||||
else {
|
||||
var tl = new TimelineMax();
|
||||
tl
|
||||
.to(mainContainer, animationSpeed, { bottom: "45%", ease: Power1.easeInOut }, 'label')
|
||||
.to(mainContainer, animationSpeed, { opacity: 0, ease: Power1.easeInOut }, 'label')
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////////////
|
||||
// STREAMER.BOT WEBSOCKET STATUS //
|
||||
///////////////////////////////////
|
||||
|
||||
// This function sets the visibility of the Streamer.bot status label on the overlay
|
||||
function SetConnectionStatus(connected) {
|
||||
let statusContainer = document.getElementById("statusContainer");
|
||||
if (connected) {
|
||||
statusContainer.style.background = "#2FB774";
|
||||
statusContainer.innerText = "Connected!";
|
||||
var tl = new TimelineMax();
|
||||
tl
|
||||
.to(statusContainer, 2, { opacity: 0, ease: Linear.easeNone });
|
||||
console.log("Connected to Cider!");
|
||||
}
|
||||
else {
|
||||
// statusContainer.style.background = "#D12025";
|
||||
// statusContainer.innerText = "Connecting...";
|
||||
// statusContainer.style.opacity = 1;
|
||||
console.log("Not connected to Cider...");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////
|
||||
// RESIZER THING BECAUSE I THINK I KNOW HOW RESPONSIVE DESIGN WORKS EVEN THOUGH I DON'T //
|
||||
//////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
let outer = document.getElementById('mainContainer'),
|
||||
maxWidth = outer.clientWidth+100,
|
||||
maxHeight = outer.clientHeight;
|
||||
|
||||
window.addEventListener("resize", resize);
|
||||
|
||||
resize();
|
||||
function resize() {
|
||||
const scale = window.innerWidth / maxWidth;
|
||||
outer.style.transform = 'translate(-50%, 50%) scale(' + scale + ')';
|
||||
}
|
||||
|
||||
|
||||
|
||||
/////////////////////////////////////////////////////////////////////
|
||||
// IF THE USER PUT IN THE HIDEALBUMART PARAMATER, THEN YOU SHOULD //
|
||||
// HIDE THE ALBUM ART, BECAUSE THAT'S WHAT IT'S SUPPOSED TO DO //
|
||||
/////////////////////////////////////////////////////////////////////
|
||||
|
||||
// if (hideAlbumArt)
|
||||
// {
|
||||
// document.getElementById("albumArtBox").style.display = "none";
|
||||
// document.getElementById("songInfoBox").style.width = "calc(100% - 20px)";
|
||||
// }
|
||||
|
||||
|
||||
connectws();
|
||||
@@ -0,0 +1,181 @@
|
||||
* {
|
||||
--corner-radius: 25px;
|
||||
--album-art-size: 50px;
|
||||
}
|
||||
|
||||
#statusContainer {
|
||||
font-family: 'Metropolis';
|
||||
font-style: italic;
|
||||
font-weight: bold;
|
||||
font-size: 30px;
|
||||
width: 400px;
|
||||
max-width: fit-content;
|
||||
text-align: center;
|
||||
background-color: #D12025;
|
||||
color: white;
|
||||
padding: 10px;
|
||||
border-radius: 10px;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
#mainContainer {
|
||||
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica, Arial, sans-serif;
|
||||
display: flex;
|
||||
position: absolute;
|
||||
height: var(--album-art-size);
|
||||
margin: 20px;
|
||||
filter: drop-shadow(15px 15px 7px rgba(0, 0, 0, 1));
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
bottom: 45%;
|
||||
left: 50%;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
#albumArtBox {
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
position: relative;
|
||||
border-radius: var(--corner-radius);
|
||||
overflow: hidden;
|
||||
margin: 0px 8px 0px 0px;
|
||||
object-fit: cover;
|
||||
width: var(--album-art-size);
|
||||
}
|
||||
|
||||
#albumArtBox img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
#songInfoBox {
|
||||
position: relative;
|
||||
color: white;
|
||||
/* width: calc(100% - 125px); */
|
||||
width: calc(100%);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 0 1 auto;
|
||||
justify-content: center;
|
||||
z-index: 1;
|
||||
text-shadow: 2px 2px 2px rgba(0, 0, 0, 0.5);
|
||||
overflow: hidden;
|
||||
z-index: 4;
|
||||
}
|
||||
|
||||
#songInfo {
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
opacity: 0.9;
|
||||
position: relative;
|
||||
border-radius: var(--corner-radius);
|
||||
padding: 0px 20px;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#IAmRunningOutOfNamesForTheseBoxes {
|
||||
position: absolute;
|
||||
width: calc(100% - 40px);
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
-ms-transform: translateY(-50%);
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
#backgroundArt {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
border-radius: var(--corner-radius);
|
||||
overflow: hidden;
|
||||
z-index: -1;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
#backgroundImage {
|
||||
filter: blur(20px);
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%) translateY(-50%);
|
||||
transition: all 2s ease;
|
||||
}
|
||||
|
||||
#backgroundImageBack {
|
||||
filter: blur(20px);
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%) translateY(-50%);
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
#artistLabel {
|
||||
font-size: 18px;
|
||||
font-weight: 400;
|
||||
line-height: 1;
|
||||
white-space: nowrap;
|
||||
transition: all 0.5s ease;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#songLabel {
|
||||
font-weight: bold;
|
||||
font-size: 20px;
|
||||
width: calc(100%);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
transition: all 0.5s ease;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* #progressBg {
|
||||
margin-top: 15px;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
border-radius: 5px;
|
||||
background-color: #1F1F1F;
|
||||
} */
|
||||
|
||||
/* #progressBar {
|
||||
border-radius: 5px;
|
||||
height: 5px;
|
||||
width: 20%;
|
||||
background-color: #ffffff;
|
||||
margin: 10px 0px;
|
||||
} */
|
||||
|
||||
/* #times {
|
||||
position: relative;
|
||||
height: 16px;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
line-height: 2.2;
|
||||
} */
|
||||
|
||||
/* #progressTime {
|
||||
position: absolute;
|
||||
} */
|
||||
|
||||
/* #duration {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
text-align: right;
|
||||
} */
|
||||
|
||||
.text-show {
|
||||
opacity: 1;
|
||||
transition: all 0.25s ease;
|
||||
}
|
||||
|
||||
.text-fade {
|
||||
opacity: 0;
|
||||
transition: all 0.25s ease;
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="shortcut icon" href="#" />
|
||||
<link rel="stylesheet" href="./style.css" />
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@xz/fonts@1/serve/metropolis.min.css" />
|
||||
<script src="https://cdn.socket.io/4.7.5/socket.io.min.js"
|
||||
integrity="sha384-2huaZvOR9iDzHqslqwpR87isEmrfxqyWOF7hr7BY6KG0+hVKLoEXMPUJw3ynWuhO"
|
||||
crossorigin="anonymous"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="statusContainer">Connecting...</div>
|
||||
<div id="mainContainer">
|
||||
<div id="albumArtBox">
|
||||
<img id="albumArt"
|
||||
src="https://static.wikia.nocookie.net/dreamcatcherwiki/images/d/dc/Dystopia_Lose_Myself_Digital_Cover.jpg"></img>
|
||||
<img id="albumArtBack"
|
||||
src="https://cdns-images.dzcdn.net/images/cover/762f7ef8a0c20e91304a3fae47449ccd/1900x1900-000000-80-0-0.jpg"></img>
|
||||
</div>
|
||||
<div id="songInfoBox">
|
||||
<div id="songInfo">
|
||||
<div id="IAmRunningOutOfNamesForTheseBoxes">
|
||||
<div id="songLabel">Can't get you out of my mind</div>
|
||||
<div id="artistLabel">Dreamcatcher</div>
|
||||
<div id="times">
|
||||
<div id="progressTime">2:29</div>
|
||||
<div id="duration">3:43</div>
|
||||
</div>
|
||||
<div id="progressBg">
|
||||
<div id="progressBar"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="backgroundArt">
|
||||
<img id="backgroundImage"
|
||||
src="https://static.wikia.nocookie.net/dreamcatcherwiki/images/d/dc/Dystopia_Lose_Myself_Digital_Cover.jpg"></img>
|
||||
<img id="backgroundImageBack"
|
||||
src="https://cdns-images.dzcdn.net/images/cover/762f7ef8a0c20e91304a3fae47449ccd/1900x1900-000000-80-0-0.jpg"></img>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
<script src='https://cdnjs.cloudflare.com/ajax/libs/gsap/1.18.0/TweenMax.min.js'></script>
|
||||
<script src="./script.js"></script>
|
||||
@@ -0,0 +1,229 @@
|
||||
///////////////
|
||||
// PARAMETRS //
|
||||
///////////////
|
||||
|
||||
const queryString = window.location.search;
|
||||
const urlParams = new URLSearchParams(queryString);
|
||||
|
||||
const visibilityDuration = urlParams.get("duration") || 0;
|
||||
const hideAlbumArt = urlParams.has("hideAlbumArt");
|
||||
|
||||
|
||||
/////////////////
|
||||
// GLOBAL VARS //
|
||||
/////////////////
|
||||
|
||||
let animationSpeed = 0.5;
|
||||
|
||||
|
||||
|
||||
//////////////////////
|
||||
// WEBSOCKET SERVER //
|
||||
//////////////////////
|
||||
|
||||
// This is the main function that connects to the Streamer.bot websocket server
|
||||
function connectws() {
|
||||
if ("WebSocket" in window) {
|
||||
//const ws = new WebSocket("ws://" + sbServerAddress + ":" + sbServerPort + "/");
|
||||
const CiderApp = io("http://localhost:10767/", {
|
||||
transports: ['websocket']
|
||||
});
|
||||
|
||||
CiderApp.on("disconnect", (event) => {
|
||||
SetConnectionStatus(false);
|
||||
setTimeout(connectws, 5000);
|
||||
});
|
||||
|
||||
CiderApp.on("connect", (event) => {
|
||||
SetConnectionStatus(true);
|
||||
});
|
||||
|
||||
// Set up websocket artwork/information handling
|
||||
CiderApp.on("API:Playback", ({ data, type }) => {
|
||||
switch (type) {
|
||||
// Song changes
|
||||
case ("playbackStatus.nowPlayingItemDidChange"):
|
||||
UpdateSongInfo(data);
|
||||
break;
|
||||
|
||||
// Progress bar moves
|
||||
case ("playbackStatus.playbackTimeDidChange"):
|
||||
UpdateProgressBar(data);
|
||||
break;
|
||||
|
||||
// Pause/unpause
|
||||
case ("playbackStatus.playbackStateDidChange"):
|
||||
UpdatePlaybackState(data);
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
////////////////////////
|
||||
// NOW PLAYING WIDGET //
|
||||
////////////////////////
|
||||
|
||||
function UpdateSongInfo(data) {
|
||||
// Set the user's info
|
||||
let albumArtUrl = data.artwork.url;
|
||||
albumArtUrl = albumArtUrl.replace("{w}", data.artwork.width);
|
||||
albumArtUrl = albumArtUrl.replace("{h}", data.artwork.height);
|
||||
|
||||
UpdateAlbumArt(document.getElementById("albumArt"), albumArtUrl);
|
||||
UpdateAlbumArt(document.getElementById("backgroundImage"), albumArtUrl);
|
||||
|
||||
setTimeout(() => {
|
||||
UpdateTextLabel(document.getElementById("songLabel"), data.name);
|
||||
UpdateTextLabel(document.getElementById("artistLabel"), data.artistName);
|
||||
}, animationSpeed * 500);
|
||||
|
||||
setTimeout(() => {
|
||||
document.getElementById("albumArtBack").src = albumArtUrl;
|
||||
document.getElementById("backgroundImageBack").src = albumArtUrl;
|
||||
}, 2 * animationSpeed * 500);
|
||||
|
||||
if (visibilityDuration > 0) {
|
||||
setTimeout(() => {
|
||||
SetVisibility(false);
|
||||
}, visibilityDuration * 1000);
|
||||
}
|
||||
}
|
||||
|
||||
function UpdateTextLabel(div, text) {
|
||||
if (div.innerHTML != text) {
|
||||
div.setAttribute("class", "text-fade");
|
||||
setTimeout(() => {
|
||||
div.innerHTML = text;
|
||||
div.setAttribute("class", ".text-show");
|
||||
}, animationSpeed * 250);
|
||||
}
|
||||
}
|
||||
|
||||
function UpdateAlbumArt(div, imgsrc) {
|
||||
if (div.src != imgsrc) {
|
||||
div.setAttribute("class", "text-fade");
|
||||
setTimeout(() => {
|
||||
div.src = imgsrc;
|
||||
div.setAttribute("class", "text-show");
|
||||
}, animationSpeed * 500);
|
||||
}
|
||||
}
|
||||
|
||||
function UpdateProgressBar(data) {
|
||||
const progress = ((data.currentPlaybackTime / data.currentPlaybackDuration) * 100);
|
||||
const progressTime = ConvertSecondsToMinutesSoThatItLooksBetterOnTheOverlay(data.currentPlaybackTime);
|
||||
const duration = ConvertSecondsToMinutesSoThatItLooksBetterOnTheOverlay(data.currentPlaybackTimeRemaining);
|
||||
document.getElementById("progressBar").style.width = `${progress}%`;
|
||||
document.getElementById("progressTime").innerHTML = progressTime;
|
||||
document.getElementById("duration").innerHTML = `-${duration}`;
|
||||
}
|
||||
|
||||
function UpdatePlaybackState(data) {
|
||||
console.log(data);
|
||||
switch (data.state) {
|
||||
case ("paused"):
|
||||
case ("stopped"):
|
||||
SetVisibility(false);
|
||||
break;
|
||||
case ("playing"):
|
||||
UpdateSongInfo(data.attributes);
|
||||
setTimeout(() => {
|
||||
SetVisibility(true);
|
||||
}, animationSpeed * 500);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
//////////////////////
|
||||
// HELPER FUNCTIONS //
|
||||
//////////////////////
|
||||
|
||||
function ConvertSecondsToMinutesSoThatItLooksBetterOnTheOverlay(time) {
|
||||
const minutes = Math.floor(time / 60);
|
||||
const seconds = Math.trunc(time - minutes * 60);
|
||||
|
||||
return `${minutes}:${('0' + seconds).slice(-2)}`;
|
||||
}
|
||||
|
||||
function SetVisibility(isVisible) {
|
||||
widgetVisibility = isVisible;
|
||||
|
||||
const mainContainer = document.getElementById("mainContainer");
|
||||
|
||||
if (isVisible) {
|
||||
var tl = new TimelineMax();
|
||||
tl
|
||||
.to(mainContainer, animationSpeed, { bottom: "50%", ease: Power1.easeInOut }, 'label')
|
||||
.to(mainContainer, animationSpeed, { opacity: 1, ease: Power1.easeInOut }, 'label')
|
||||
}
|
||||
else {
|
||||
var tl = new TimelineMax();
|
||||
tl
|
||||
.to(mainContainer, animationSpeed, { bottom: "45%", ease: Power1.easeInOut }, 'label')
|
||||
.to(mainContainer, animationSpeed, { opacity: 0, ease: Power1.easeInOut }, 'label')
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////////////
|
||||
// STREAMER.BOT WEBSOCKET STATUS //
|
||||
///////////////////////////////////
|
||||
|
||||
// This function sets the visibility of the Streamer.bot status label on the overlay
|
||||
function SetConnectionStatus(connected) {
|
||||
let statusContainer = document.getElementById("statusContainer");
|
||||
if (connected) {
|
||||
statusContainer.style.background = "#2FB774";
|
||||
statusContainer.innerText = "Connected!";
|
||||
var tl = new TimelineMax();
|
||||
tl
|
||||
.to(statusContainer, 2, { opacity: 0, ease: Linear.easeNone });
|
||||
console.log("Connected to Cider!");
|
||||
}
|
||||
else {
|
||||
// statusContainer.style.background = "#D12025";
|
||||
// statusContainer.innerText = "Connecting...";
|
||||
// statusContainer.style.opacity = 1;
|
||||
console.log("Not connected to Cider...");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////
|
||||
// RESIZER THING BECAUSE I THINK I KNOW HOW RESPONSIVE DESIGN WORKS EVEN THOUGH I DON'T //
|
||||
//////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
let outer = document.getElementById('mainContainer'),
|
||||
maxWidth = outer.clientWidth+50,
|
||||
maxHeight = outer.clientHeight;
|
||||
|
||||
window.addEventListener("resize", resize);
|
||||
|
||||
resize();
|
||||
function resize() {
|
||||
const scale = window.innerWidth / maxWidth;
|
||||
outer.style.transform = 'translate(-50%, 50%) scale(' + scale + ')';
|
||||
}
|
||||
|
||||
|
||||
|
||||
/////////////////////////////////////////////////////////////////////
|
||||
// IF THE USER PUT IN THE HIDEALBUMART PARAMATER, THEN YOU SHOULD //
|
||||
// HIDE THE ALBUM ART, BECAUSE THAT'S WHAT IT'S SUPPOSED TO DO //
|
||||
/////////////////////////////////////////////////////////////////////
|
||||
|
||||
if (hideAlbumArt)
|
||||
{
|
||||
document.getElementById("albumArtBox").style.display = "none";
|
||||
document.getElementById("songInfoBox").style.width = "calc(100% - 20px)";
|
||||
}
|
||||
|
||||
|
||||
connectws();
|
||||
@@ -0,0 +1,177 @@
|
||||
* {
|
||||
--corner-radius: 10px;
|
||||
--album-art-size: 100px;
|
||||
}
|
||||
|
||||
#statusContainer {
|
||||
font-family: 'Metropolis';
|
||||
font-style: italic;
|
||||
font-weight: bold;
|
||||
font-size: 30px;
|
||||
width: 400px;
|
||||
max-width: fit-content;
|
||||
text-align: center;
|
||||
background-color: #D12025;
|
||||
color: white;
|
||||
padding: 10px;
|
||||
border-radius: 10px;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
#mainContainer {
|
||||
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica, Arial, sans-serif;
|
||||
display: flex;
|
||||
position: absolute;
|
||||
height: var(--album-art-size);
|
||||
margin: 20px;
|
||||
filter: drop-shadow(15px 15px 7px rgba(0, 0, 0, 1));
|
||||
width: 100%;
|
||||
max-width: 500px;
|
||||
bottom: 45%;
|
||||
left: 50%;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
#albumArtBox {
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
position: relative;
|
||||
border-radius: var(--corner-radius);
|
||||
overflow: hidden;
|
||||
margin: 0px 8px 0px 0px;
|
||||
object-fit: cover;
|
||||
width: var(--album-art-size);
|
||||
}
|
||||
|
||||
#albumArtBox img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
#songInfoBox {
|
||||
position: relative;
|
||||
color: white;
|
||||
width: calc(100% - 125px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 0 1 auto;
|
||||
justify-content: center;
|
||||
z-index: 1;
|
||||
text-shadow: 2px 2px 2px rgba(0, 0, 0, 0.5);
|
||||
overflow: hidden;
|
||||
z-index: 4;
|
||||
}
|
||||
|
||||
#songInfo {
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
opacity: 0.9;
|
||||
position: relative;
|
||||
border-radius: var(--corner-radius);
|
||||
padding: 0px 20px;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#IAmRunningOutOfNamesForTheseBoxes {
|
||||
position: absolute;
|
||||
width: calc(100% - 40px);
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
-ms-transform: translateY(-50%);
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
#backgroundArt {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
border-radius: var(--corner-radius);
|
||||
overflow: hidden;
|
||||
z-index: -1;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
#backgroundImage {
|
||||
filter: blur(20px);
|
||||
position: absolute;
|
||||
width: 140%;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%) translateY(-50%);
|
||||
}
|
||||
|
||||
#backgroundImageBack {
|
||||
filter: blur(20px);
|
||||
position: absolute;
|
||||
width: 140%;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%) translateY(-50%);
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
#artistLabel {
|
||||
font-size: 16px;
|
||||
font-weight: 400;
|
||||
line-height: 1;
|
||||
white-space: nowrap;
|
||||
transition: all 0.5s ease;
|
||||
}
|
||||
|
||||
#songLabel {
|
||||
font-weight: bold;
|
||||
font-size: 20px;
|
||||
width: calc(100%);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
transition: all 0.5s ease;
|
||||
}
|
||||
|
||||
#progressBg {
|
||||
margin-top: 15px;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
border-radius: 5px;
|
||||
background-color: #1F1F1F;
|
||||
}
|
||||
|
||||
#progressBar {
|
||||
border-radius: 5px;
|
||||
height: 5px;
|
||||
width: 20%;
|
||||
background-color: #ffffff;
|
||||
margin: 10px 0px;
|
||||
}
|
||||
|
||||
#times {
|
||||
position: relative;
|
||||
height: 16px;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
line-height: 2.2;
|
||||
}
|
||||
|
||||
#progressTime {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
#duration {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.text-show {
|
||||
opacity: 1;
|
||||
transition: all 0.25s ease;
|
||||
}
|
||||
|
||||
.text-fade {
|
||||
opacity: 0;
|
||||
transition: all 0.25s ease;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
|
||||
<link rel="stylesheet" href="./style.css" />
|
||||
<div id="mainContainer">
|
||||
<label id="timeLabel">Balls</label>
|
||||
</div>
|
||||
|
||||
<script src='./moment.js'></script>
|
||||
<script src="./script.js"></script>
|
||||
@@ -0,0 +1,19 @@
|
||||
////////////////////
|
||||
// URL PARAMETERS //
|
||||
////////////////////
|
||||
|
||||
const queryString = window.location.search;
|
||||
const urlParams = new URLSearchParams(queryString);
|
||||
const dateFormat = urlParams.get("dateFormat") || 'ddd DD MMM yyyy hh:mm:ss A';
|
||||
|
||||
///////////////
|
||||
// FUNCTIONS //
|
||||
///////////////
|
||||
|
||||
function setTime()
|
||||
{
|
||||
document.getElementById("timeLabel").innerHTML = moment().format(dateFormat);
|
||||
setTimeout(setTime, 1000);
|
||||
}
|
||||
|
||||
setTime();
|
||||
@@ -0,0 +1,19 @@
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
#timeLabel {
|
||||
font-size: 40px;
|
||||
text-transform: uppercase;
|
||||
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica, Arial, sans-serif;
|
||||
|
||||
font-weight: 700;
|
||||
font-size: 36px;
|
||||
text-shadow: rgb(0, 0, 0) 2px 2px 2px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
#mainContainer {
|
||||
text-align: center;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 24 24" width="20" height="20" aria-label="Icon" role="img"><path fill-rule="evenodd" d="M18 6a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-1.959l2.75 1.588A1.5 1.5 0 0 0 23 16.33V7.67a1.5 1.5 0 0 0-2.25-1.3L18 7.96z" clip-rule="evenodd"></path></svg>
|
||||
|
After Width: | Height: | Size: 343 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><!-- Icon from Material Design Icons by Pictogrammers - https://github.com/Templarian/MaterialDesign/blob/master/LICENSE --><path fill="currentColor" d="M12 17.27L18.18 21l-1.64-7.03L22 9.24l-7.19-.62L12 2L9.19 8.62L2 9.24l5.45 4.73L5.82 21z"/></svg>
|
||||
|
After Width: | Height: | Size: 333 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><!-- Icon from Material Design Icons by Pictogrammers - https://github.com/Templarian/MaterialDesign/blob/master/LICENSE --><path fill="currentColor" d="M6.92 5H5l9 9l1-.94m4.96 6.06l-.84.84a.996.996 0 0 1-1.41 0l-3.12-3.12l-2.68 2.66l-1.41-1.41l1.42-1.42L3 7.75V3h4.75l8.92 8.92l1.42-1.42l1.41 1.41l-2.67 2.67l3.12 3.12c.4.4.4 1.03.01 1.42"/></svg>
|
||||
|
After Width: | Height: | Size: 432 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><!-- Icon from All by undefined - undefined --><path fill="currentColor" fill-rule="evenodd" d="M9.592 3.2a6 6 0 0 1-.495.399c-.298.2-.633.338-.985.408c-.153.03-.313.043-.632.068c-.801.064-1.202.096-1.536.214a2.71 2.71 0 0 0-1.655 1.655c-.118.334-.15.735-.214 1.536a6 6 0 0 1-.068.632c-.07.352-.208.687-.408.985c-.087.13-.191.252-.399.495c-.521.612-.782.918-.935 1.238c-.353.74-.353 1.6 0 2.34c.153.32.414.626.935 1.238c.208.243.312.365.399.495c.2.298.338.633.408.985c.03.153.043.313.068.632c.064.801.096 1.202.214 1.536a2.71 2.71 0 0 0 1.655 1.655c.334.118.735.15 1.536.214c.319.025.479.038.632.068c.352.07.687.209.985.408c.13.087.252.191.495.399c.612.521.918.782 1.238.935c.74.353 1.6.353 2.34 0c.32-.153.626-.414 1.238-.935c.243-.208.365-.312.495-.399c.298-.2.633-.338.985-.408c.153-.03.313-.043.632-.068c.801-.064 1.202-.096 1.536-.214a2.71 2.71 0 0 0 1.655-1.655c.118-.334.15-.735.214-1.536c.025-.319.038-.479.068-.632c.07-.352.209-.687.408-.985c.087-.13.191-.252.399-.495c.521-.612.782-.918.935-1.238c.353-.74.353-1.6 0-2.34c-.153-.32-.414-.626-.935-1.238a6 6 0 0 1-.399-.495a2.7 2.7 0 0 1-.408-.985a6 6 0 0 1-.068-.632c-.064-.801-.096-1.202-.214-1.536a2.71 2.71 0 0 0-1.655-1.655c-.334-.118-.735-.15-1.536-.214a6 6 0 0 1-.632-.068a2.7 2.7 0 0 1-.985-.408a6 6 0 0 1-.495-.399c-.612-.521-.918-.782-1.238-.935a2.71 2.71 0 0 0-2.34 0c-.32.153-.626.414-1.238.935m6.781 6.663a.814.814 0 0 0-1.15-1.15l-4.85 4.85l-1.596-1.595a.814.814 0 0 0-1.15 1.15l2.17 2.17a.814.814 0 0 0 1.15 0z" clip-rule="evenodd"/></svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 8.4 KiB |
@@ -0,0 +1,38 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="shortcut icon" href="#" />
|
||||
<link rel="stylesheet" href="./style.css" />
|
||||
<script type="text/javascript" src="https://unpkg.com/@streamerbot/client/dist/streamerbot-client.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="statusContainer">Connecting...</div>
|
||||
<div id="mainContainer" class="slide-fade">
|
||||
<ul id="messageList">
|
||||
</ul>
|
||||
<div id="alertBox"></div>
|
||||
<div id="IPutThisHereSoICanCalculateHowBigEachMessageIsSupposedToBeBeforeIAddItToTheMessageList">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<template id="messageTemplate">
|
||||
<div style="display: inline;">
|
||||
<label id="userInfo" style="margin-left: 10px;">
|
||||
<span id="platform"></span>
|
||||
<span id="avatar"></span>
|
||||
<span id="timestamp"></span>
|
||||
<span id="badgeList"></span>
|
||||
<span id="pronouns"></span>
|
||||
<span id="username"></span>
|
||||
<span id="colon-separator">: </span>
|
||||
</label>
|
||||
<span id="message"></span>
|
||||
</div>
|
||||
</template>
|
||||
</body>
|
||||
|
||||
<script src="./script.js"></script>
|
||||
@@ -0,0 +1,881 @@
|
||||
////////////////
|
||||
// PARAMETERS //
|
||||
////////////////
|
||||
|
||||
const queryString = window.location.search;
|
||||
const urlParams = new URLSearchParams(queryString);
|
||||
|
||||
const sbServerAddress = urlParams.get("address") || "127.0.0.1";
|
||||
const sbServerPort = urlParams.get("port") || "8080";
|
||||
const minimumRole = 2; // 1 - Viewer, 2 - VIP, 3 - Moderator, 4 - Broadcaster
|
||||
const avatarMap = new Map();
|
||||
const animationDuration = 8000;
|
||||
let widgetLocked = false; // Needed to lock animation from overlapping
|
||||
let alertQueue = [];
|
||||
|
||||
/////////////
|
||||
// OPTIONS //
|
||||
/////////////
|
||||
|
||||
const showPlatform = GetBooleanParam("showPlatform", true);
|
||||
const showAvatar = GetBooleanParam("showAvatar", true);
|
||||
const showTimestamps = GetBooleanParam("showTimestamps", false);
|
||||
const showBadges = GetBooleanParam("showBadges", true);
|
||||
const showPronouns = GetBooleanParam("showPronouns", false);
|
||||
const showUsername = GetBooleanParam("showUsername", true);
|
||||
const showMessage = GetBooleanParam("showMessage", true);
|
||||
const font = urlParams.get("font") || "";
|
||||
const fontSize = urlParams.get("fontSize") || "18";
|
||||
|
||||
const hideAfter = GetIntParam("hideAfter") || 0;
|
||||
const excludeCommands = GetBooleanParam("excludeCommands", true);
|
||||
const ignoreChatters = urlParams.get("ignoreChatters") || "";
|
||||
|
||||
const showTwitchMessages = GetBooleanParam("showTwitchMessages", true);
|
||||
const showTwitchAnnouncements = GetBooleanParam("showTwitchAnnouncements", true);
|
||||
const showTwitchSubs = GetBooleanParam("showTwitchSubs", true);
|
||||
const showTwitchRaids = GetBooleanParam("showTwitchRaids", true);
|
||||
|
||||
const showYouTubeMessages = GetBooleanParam("showYouTubeMessages", true);
|
||||
const showYouTubeSuperChats = GetBooleanParam("showYouTubeSuperChats", true);
|
||||
const showYouTubeSuperStickers = GetBooleanParam("showYouTubeSuperStickers", true);
|
||||
const showYouTubeMemberships = GetBooleanParam("showYouTubeMemberships", true);
|
||||
|
||||
const showStreamlabsDonations = GetBooleanParam("showStreamlabsDonations", true)
|
||||
const showStreamElementsTips = GetBooleanParam("showStreamElementsTips", true);
|
||||
|
||||
// Set fonts for the widget
|
||||
document.body.style.fontFamily = font;
|
||||
document.body.style.fontSize = `${fontSize}px`;
|
||||
|
||||
// Get a list of chatters to ignore
|
||||
const ignoreUserList = ignoreChatters.split(',').map(item => item.trim().toLowerCase()) || [];
|
||||
|
||||
|
||||
|
||||
/////////////////////////
|
||||
// STREAMER.BOT CLIENT //
|
||||
/////////////////////////
|
||||
|
||||
const client = new StreamerbotClient({
|
||||
host: sbServerAddress,
|
||||
port: sbServerPort,
|
||||
|
||||
onConnect: (data) => {
|
||||
console.log(`Streamer.bot successfully connected to ${sbServerAddress}:${sbServerPort}`)
|
||||
console.debug(data);
|
||||
SetConnectionStatus(true);
|
||||
},
|
||||
|
||||
onDisconnect: () => {
|
||||
console.error(`Streamer.bot disconnected from ${sbServerAddress}:${sbServerPort}`)
|
||||
SetConnectionStatus(false);
|
||||
}
|
||||
});
|
||||
|
||||
client.on('Twitch.ChatMessage', (response) => {
|
||||
console.debug(response.data);
|
||||
TwitchChatMessage(response.data);
|
||||
})
|
||||
|
||||
client.on('Twitch.Cheer', (response) => {
|
||||
console.debug(response.data);
|
||||
TwitchChatMessage(response.data);
|
||||
})
|
||||
|
||||
client.on('Twitch.Announcement', (response) => {
|
||||
console.debug(response.data);
|
||||
TwitchAnnouncement(response.data);
|
||||
})
|
||||
|
||||
client.on('Twitch.Sub', (response) => {
|
||||
console.debug(response.data);
|
||||
TwitchSub(response.data);
|
||||
})
|
||||
|
||||
client.on('Twitch.ReSub', (response) => {
|
||||
console.debug(response.data);
|
||||
TwitchResub(response.data);
|
||||
})
|
||||
|
||||
client.on('Twitch.GiftSub', (response) => {
|
||||
console.debug(response.data);
|
||||
TwitchGiftSub(response.data);
|
||||
})
|
||||
|
||||
client.on('Twitch.GiftBomb', (response) => {
|
||||
console.debug(response.data);
|
||||
TwitchGiftBomb(response.data);
|
||||
})
|
||||
|
||||
client.on('Twitch.Raid', (response) => {
|
||||
console.debug(response.data);
|
||||
TwitchRaid(response.data);
|
||||
})
|
||||
|
||||
client.on('Twitch.ChatMessageDeleted', (response) => {
|
||||
console.debug(response.data);
|
||||
TwitchChatMessageDeleted(response.data);
|
||||
})
|
||||
|
||||
client.on('Twitch.UserBanned', (response) => {
|
||||
console.debug(response.data);
|
||||
TwitchUserBanned(response.data);
|
||||
})
|
||||
|
||||
client.on('Twitch.UserTimedOut', (response) => {
|
||||
console.debug(response.data);
|
||||
TwitchUserBanned(response.data);
|
||||
})
|
||||
|
||||
client.on('Twitch.ChatCleared', (response) => {
|
||||
console.debug(response.data);
|
||||
TwitchChatCleared(response.data);
|
||||
})
|
||||
|
||||
client.on('YouTube.Message', (response) => {
|
||||
console.debug(response.data);
|
||||
YouTubeMessage(response.data)
|
||||
})
|
||||
|
||||
client.on('YouTube.SuperChat', (response) => {
|
||||
console.debug(response.data);
|
||||
YouTubeSuperChat(response.data);
|
||||
})
|
||||
|
||||
client.on('YouTube.SuperSticker', (response) => {
|
||||
console.debug(response.data);
|
||||
YouTubeSuperSticker(response.data);
|
||||
})
|
||||
|
||||
client.on('YouTube.NewSponsor', (response) => {
|
||||
console.debug(response.data);
|
||||
YouTubeNewSponsor(response.data);
|
||||
})
|
||||
|
||||
client.on('YouTube.GiftMembershipReceived', (response) => {
|
||||
console.debug(response.data);
|
||||
YouTubeGiftMembershipReceived();
|
||||
})
|
||||
|
||||
client.on('Streamlabs.Donation', (response) => {
|
||||
console.debug(response.data);
|
||||
StreamlabsDonation(response.data);
|
||||
})
|
||||
|
||||
client.on('StreamElements.Tip', (response) => {
|
||||
console.debug(response.data);
|
||||
StreamElementsTip(response.data);
|
||||
})
|
||||
|
||||
|
||||
|
||||
/////////////////////
|
||||
// HORIZONTAL CHAT //
|
||||
/////////////////////
|
||||
|
||||
async function TwitchChatMessage(data) {
|
||||
if (!showTwitchMessages)
|
||||
return;
|
||||
|
||||
// Don't post messages starting with "!"
|
||||
if (data.message.message.startsWith("!") && excludeCommands)
|
||||
return;
|
||||
|
||||
// Don't post messages from users from the ignore list
|
||||
if (ignoreUserList.includes(data.message.username))
|
||||
return;
|
||||
|
||||
// Get a reference to the template
|
||||
const template = document.getElementById('messageTemplate');
|
||||
|
||||
// Create a new instance of the template
|
||||
const instance = template.content.cloneNode(true);
|
||||
|
||||
// Get divs
|
||||
const messageContainer = instance.querySelector(".messageContainer");
|
||||
const userInfoDiv = instance.querySelector("#userInfo");
|
||||
const avatarDiv = instance.querySelector("#avatar");
|
||||
const timestampDiv = instance.querySelector("#timestamp");
|
||||
const platformDiv = instance.querySelector("#platform");
|
||||
const badgeListDiv = instance.querySelector("#badgeList");
|
||||
const pronounsDiv = instance.querySelector("#pronouns");
|
||||
const usernameDiv = instance.querySelector("#username");
|
||||
const messageDiv = instance.querySelector("#message");
|
||||
|
||||
// Set timestamp
|
||||
if (showTimestamps) {
|
||||
timestampDiv.classList.add("timestamp");
|
||||
timestampDiv.innerText = GetCurrentTimeFormatted();
|
||||
}
|
||||
|
||||
// Set the username info
|
||||
if (showUsername) {
|
||||
usernameDiv.innerText = data.message.displayName;
|
||||
usernameDiv.style.color = data.message.color;
|
||||
}
|
||||
|
||||
// Set pronouns
|
||||
const pronouns = await GetPronouns('twitch', 'caffeinedaydream');
|
||||
if (pronouns && showPronouns) {
|
||||
pronounsDiv.classList.add("pronouns");
|
||||
pronounsDiv.innerText = pronouns;
|
||||
}
|
||||
|
||||
// Set the message data
|
||||
const message = data.message.message;
|
||||
const messageColor = data.message.color;
|
||||
|
||||
// Set message text
|
||||
if (showMessage) {
|
||||
messageDiv.innerText = message;
|
||||
}
|
||||
|
||||
// Set the "action" color
|
||||
if (data.message.isMe)
|
||||
messageDiv.style.color = messageColor;
|
||||
|
||||
// Render platform
|
||||
if (showPlatform) {
|
||||
const platformElements = `<img src="icons/platforms/twitch.png" class="platform"/>`;
|
||||
platformDiv.innerHTML = platformElements;
|
||||
}
|
||||
|
||||
|
||||
// Render badges
|
||||
if (showBadges) {
|
||||
badgeListDiv.innerHTML = "";
|
||||
for (i in data.message.badges) {
|
||||
const badge = new Image();
|
||||
badge.src = data.message.badges[i].imageUrl;
|
||||
badge.classList.add("badge");
|
||||
badgeListDiv.appendChild(badge);
|
||||
}
|
||||
}
|
||||
|
||||
// Render emotes
|
||||
for (i in data.emotes) {
|
||||
const emoteElement = `<img src="${data.emotes[i].imageUrl}" class="emote"/>`;
|
||||
messageDiv.innerHTML = messageDiv.innerHTML.replace(new RegExp(`\\b${data.emotes[i].name}\\b`), emoteElement);
|
||||
}
|
||||
|
||||
// Render cheermotes
|
||||
for (i in data.cheerEmotes) {
|
||||
// const cheerEmoteElement = `<img src="${data.cheerEmotes[i].imageUrl}" class="emote"/>`;
|
||||
// messageDiv.innerHTML = messageDiv.innerHTML.replace(new RegExp(`\\b${data.cheerEmotes[i].name}\\b`), cheerEmoteElement);
|
||||
const bits = data.cheerEmotes[i].bits;
|
||||
const imageUrl = data.cheerEmotes[i].imageUrl;
|
||||
const name = data.cheerEmotes[i].name;
|
||||
const cheerEmoteElement = `<img src="${imageUrl}" class="emote"/>`;
|
||||
const bitsElements = `<span class="bits">${bits}</span>`
|
||||
messageDiv.innerHTML = messageDiv.innerHTML.replace(new RegExp(`\\b${name}${bits}\\b`, 'i'), cheerEmoteElement + bitsElements);
|
||||
}
|
||||
|
||||
// Render avatars
|
||||
if (showAvatar) {
|
||||
const username = data.message.username;
|
||||
const avatarURL = await GetAvatar(username);
|
||||
const avatar = new Image();
|
||||
avatar.src = avatarURL;
|
||||
avatar.classList.add("avatar");
|
||||
avatarDiv.appendChild(avatar);
|
||||
}
|
||||
|
||||
// Hide the header if the same username sends a message twice in a row
|
||||
const messageList = document.getElementById("messageList");
|
||||
if (messageList.children.length > 0) {
|
||||
const lastPlatform = messageList.lastChild.dataset.platform;
|
||||
const lastUserId = messageList.lastChild.dataset.userId;
|
||||
if (lastPlatform == "twitch" && lastUserId == data.user.id)
|
||||
userInfoDiv.style.display = "none";
|
||||
}
|
||||
|
||||
AddMessageItem(instance, data.message.msgId, 'twitch', data.user.id);
|
||||
}
|
||||
|
||||
async function TwitchAnnouncement(data) {
|
||||
if (!showTwitchAnnouncements)
|
||||
return;
|
||||
|
||||
let background = null;
|
||||
|
||||
// Set the card background colors
|
||||
switch (data.announcementColor) {
|
||||
case "BLUE":
|
||||
background = 'announcementBlue';
|
||||
break;
|
||||
case "GREEN":
|
||||
background = 'announcementGreen';
|
||||
break;
|
||||
case "ORANGE":
|
||||
background = 'announcementOrange';
|
||||
break;
|
||||
case "PURPLE":
|
||||
background = 'announcementPurple';
|
||||
break;
|
||||
}
|
||||
|
||||
let message = data.text;
|
||||
|
||||
// Render emotes
|
||||
for (i in data.parts) {
|
||||
if (data.parts[i].type == `emote`) {
|
||||
const emoteElement = `<img src="${data.parts[i].imageUrl}" class="emote"/>`;
|
||||
message = message.replace(new RegExp(`\\b${data.parts[i].text}\\b`), emoteElement);
|
||||
}
|
||||
}
|
||||
|
||||
ShowAlert(message, background);
|
||||
}
|
||||
|
||||
async function TwitchSub(data) {
|
||||
if (!showTwitchSubs)
|
||||
return;
|
||||
|
||||
const username = data.user.name;
|
||||
const subTier = data.sub_tier;
|
||||
const isPrime = data.is_prime;
|
||||
|
||||
let message = '';
|
||||
|
||||
if (!isPrime)
|
||||
message = `${username} subscribed with Tier ${subTier.charAt(0)}`;
|
||||
else
|
||||
message = `${username} used their Prime Sub`, 'twitch';
|
||||
|
||||
ShowAlert(message, 'twitch');
|
||||
}
|
||||
|
||||
async function TwitchResub(data) {
|
||||
if (!showTwitchSubs)
|
||||
return;
|
||||
|
||||
const username = data.user.name;
|
||||
const subTier = data.subTier;
|
||||
const isPrime = data.isPrime;
|
||||
const cumulativeMonths = data.cumulativeMonths;
|
||||
|
||||
let message = '';
|
||||
|
||||
if (!isPrime)
|
||||
message = `${username} resubscribed with Tier ${subTier.charAt(0)} (${cumulativeMonths} months)`;
|
||||
else
|
||||
message = `${username} used their Prime Sub (${cumulativeMonths} months)`;
|
||||
|
||||
ShowAlert(message, 'twitch');
|
||||
}
|
||||
|
||||
async function TwitchGiftSub(data) {
|
||||
if (!showTwitchSubs)
|
||||
return;
|
||||
|
||||
const username = data.user.name;
|
||||
const subTier = data.subTier;
|
||||
const recipient = data.recipient.name;
|
||||
const fromCommunitySubGift = data.fromCommunitySubGift;
|
||||
|
||||
// Don't post alerts for gift bombs
|
||||
if (fromCommunitySubGift)
|
||||
return;
|
||||
|
||||
let message = `🎁 ${username} gifted a Tier ${subTier.charAt(0)} subscription to ${recipient}`;
|
||||
|
||||
ShowAlert(message, 'twitch');
|
||||
}
|
||||
|
||||
async function TwitchGiftBomb(data) {
|
||||
if (!showTwitchSubs)
|
||||
return;
|
||||
|
||||
const username = data.displayName;
|
||||
const gifts = data.gifts;
|
||||
const subTier = data.subTier;
|
||||
|
||||
let message = `🎁 ${username} gifted ${gifts} Tier ${subTier} subs!`;
|
||||
|
||||
ShowAlert(message, 'twitch');
|
||||
}
|
||||
|
||||
async function TwitchRaid(data) {
|
||||
if (!showTwitchRaids)
|
||||
return;
|
||||
|
||||
const username = data.from_broadcaster_user_login;
|
||||
const viewers = data.viewers;
|
||||
|
||||
let message = `${username} is raiding with a party of ${viewers}`;
|
||||
|
||||
ShowAlert(message, 'twitch');
|
||||
}
|
||||
|
||||
function TwitchChatMessageDeleted(data) {
|
||||
const messageList = document.getElementById("messageList");
|
||||
|
||||
// Maintain a list of chat messages to delete
|
||||
const messagesToRemove = [];
|
||||
|
||||
// ID of the message to remove
|
||||
const messageId = data.messageId;
|
||||
|
||||
// Find the items to remove
|
||||
for (let i = 0; i < messageList.children.length; i++) {
|
||||
if (messageList.children[i].id === messageId) {
|
||||
messagesToRemove.push(messageList.children[i]);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove the items
|
||||
messagesToRemove.forEach(item => {
|
||||
messageList.removeChild(item);
|
||||
});
|
||||
}
|
||||
|
||||
function TwitchUserBanned(data) {
|
||||
const messageList = document.getElementById("messageList");
|
||||
|
||||
// Maintain a list of chat messages to delete
|
||||
const messagesToRemove = [];
|
||||
|
||||
// ID of the message to remove
|
||||
const userId = data.user_id;
|
||||
|
||||
// Find the items to remove
|
||||
for (let i = 0; i < messageList.children.length; i++) {
|
||||
if (messageList.children[i].dataset.userId === userId) {
|
||||
messagesToRemove.push(messageList.children[i]);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove the items
|
||||
messagesToRemove.forEach(item => {
|
||||
messageList.removeChild(item);
|
||||
});
|
||||
}
|
||||
|
||||
function TwitchChatCleared(data) {
|
||||
const messageList = document.getElementById("messageList");
|
||||
|
||||
while (messageList.firstChild) {
|
||||
messageList.removeChild(messageList.firstChild);
|
||||
}
|
||||
}
|
||||
|
||||
function YouTubeMessage(data) {
|
||||
if (!showYouTubeMessages)
|
||||
return;
|
||||
|
||||
// Don't post messages starting with "!"
|
||||
if (data.message.startsWith("!") && excludeCommands)
|
||||
return;
|
||||
|
||||
// Don't post messages from users from the ignore list
|
||||
if (ignoreUserList.includes(data.user.name))
|
||||
return;
|
||||
|
||||
// Get a reference to the template
|
||||
const template = document.getElementById('messageTemplate');
|
||||
|
||||
// Create a new instance of the template
|
||||
const instance = template.content.cloneNode(true);
|
||||
|
||||
// Get divs
|
||||
const userInfoDiv = instance.querySelector("#userInfo");
|
||||
const avatarDiv = instance.querySelector("#avatar");
|
||||
const timestampDiv = instance.querySelector("#timestamp");
|
||||
const platformDiv = instance.querySelector("#platform");
|
||||
const badgeListDiv = instance.querySelector("#badgeList");
|
||||
const usernameDiv = instance.querySelector("#username");
|
||||
const messageDiv = instance.querySelector("#message");
|
||||
|
||||
// Set timestamp
|
||||
if (showTimestamps) {
|
||||
timestampDiv.classList.add("timestamp");
|
||||
timestampDiv.innerText = GetCurrentTimeFormatted();
|
||||
}
|
||||
|
||||
// Set the message data
|
||||
if (showUsername) {
|
||||
usernameDiv.innerText = data.user.name;
|
||||
usernameDiv.style.color = "#f70000"; // YouTube users do not have colors, so just set it to red
|
||||
}
|
||||
|
||||
if (showMessage) {
|
||||
messageDiv.innerText = data.message;
|
||||
}
|
||||
|
||||
// Render platform
|
||||
if (showPlatform) {
|
||||
const platformElements = `<img src="icons/platforms/youtube.png" class="platform"/>`;
|
||||
platformDiv.innerHTML = platformElements;
|
||||
}
|
||||
|
||||
// Render badges
|
||||
if (data.user.isOwner && showBadges) {
|
||||
const badge = new Image();
|
||||
badge.src = `icons/badges/youtube-broadcaster.svg`;
|
||||
badge.style.filter = `invert(100%)`;
|
||||
badge.style.opacity = 0.8;
|
||||
badge.classList.add("badge");
|
||||
badgeListDiv.appendChild(badge);
|
||||
}
|
||||
|
||||
if (data.user.isModerator && showBadges) {
|
||||
const badge = new Image();
|
||||
badge.src = `icons/badges/youtube-moderator.svg`;
|
||||
badge.style.filter = `invert(100%)`;
|
||||
badge.style.opacity = 0.8;
|
||||
badge.classList.add("badge");
|
||||
badgeListDiv.appendChild(badge);
|
||||
}
|
||||
|
||||
if (data.user.isSponsor && showBadges) {
|
||||
const badge = new Image();
|
||||
badge.src = `icons/badges/youtube-member.svg`;
|
||||
badge.style.filter = `invert(100%)`;
|
||||
badge.style.opacity = 0.8;
|
||||
badge.classList.add("badge");
|
||||
badgeListDiv.appendChild(badge);
|
||||
}
|
||||
|
||||
if (data.user.isVerified && showBadges) {
|
||||
const badge = new Image();
|
||||
badge.src = `icons/badges/youtube-verified.svg`;
|
||||
badge.style.filter = `invert(100%)`;
|
||||
badge.style.opacity = 0.8;
|
||||
badge.classList.add("badge");
|
||||
badgeListDiv.appendChild(badge);
|
||||
}
|
||||
|
||||
// Render emotes
|
||||
for (i in data.emotes) {
|
||||
const emoteElement = `<img src="${data.emotes[i].imageUrl}" class="emote"/>`;
|
||||
messageDiv.innerHTML = messageDiv.innerHTML.replace(data.emotes[i].name, emoteElement);
|
||||
}
|
||||
|
||||
// Render avatars
|
||||
if (showAvatar) {
|
||||
const avatar = new Image();
|
||||
avatar.src = data.user.profileImageUrl;
|
||||
avatar.classList.add("avatar");
|
||||
avatarDiv.appendChild(avatar);
|
||||
}
|
||||
|
||||
// Hide the header if the same username sends a message twice in a row
|
||||
const messageList = document.getElementById("messageList");
|
||||
if (messageList.children.length > 0) {
|
||||
const lastPlatform = messageList.lastChild.dataset.platform;
|
||||
const lastUserId = messageList.lastChild.dataset.userId;
|
||||
if (lastPlatform == "youtube" && lastUserId == data.user.id)
|
||||
userInfoDiv.style.display = "none";
|
||||
}
|
||||
|
||||
AddMessageItem(instance, data.eventId, 'youtube', data.user.id);
|
||||
}
|
||||
|
||||
function YouTubeSuperChat(data) {
|
||||
if (!showYouTubeSuperChats)
|
||||
return;
|
||||
|
||||
let message = `🪙 ${data.user.name} sent a Super Chat (${data.amount})`;
|
||||
|
||||
ShowAlert(message, 'youtube');
|
||||
}
|
||||
|
||||
function YouTubeSuperSticker(data) {
|
||||
if (!showYouTubeSuperStickers)
|
||||
return;
|
||||
|
||||
let message = `${data.user.name} sent a Super Sticker (${data.amount})`;
|
||||
|
||||
ShowAlert(message, 'youtube');
|
||||
}
|
||||
|
||||
function YouTubeNewSponsor(data) {
|
||||
if (!showYouTubeMemberships)
|
||||
return;
|
||||
|
||||
// Set message text
|
||||
let message = `⭐ New ${data.levelName} • Welcome ${data.user.name}!`;
|
||||
|
||||
ShowAlert(message, 'youtube');
|
||||
}
|
||||
|
||||
function YouTubeGiftMembershipReceived(data) {
|
||||
if (!showYouTubeMemberships)
|
||||
return;
|
||||
|
||||
let message = `🎁 ${data.gifter.name} gifted a membership to ${data.user.name} (${data.tier})!`;
|
||||
|
||||
ShowAlert(message, 'youtube');
|
||||
}
|
||||
|
||||
function StreamlabsDonation(data) {
|
||||
if (!showStreamlabsDonations)
|
||||
return;
|
||||
|
||||
const donater = data.from;
|
||||
const formattedAmount = data.formattedAmount;
|
||||
const currency = data.currency;
|
||||
|
||||
let message = `🪙 ${donater} donated ${currency}${formattedAmount}`;
|
||||
|
||||
ShowAlert(message, 'streamlabs');
|
||||
}
|
||||
|
||||
function StreamElementsTip(data) {
|
||||
if (!showStreamElementsTips)
|
||||
return;
|
||||
|
||||
const donater = data.username;
|
||||
const formattedAmount = `$${data.amount}`;
|
||||
const currency = data.currency;
|
||||
|
||||
let message = `🪙 ${donater} donated ${currency}${formattedAmount}`;
|
||||
|
||||
ShowAlert(message, 'streamelements');
|
||||
}
|
||||
|
||||
|
||||
|
||||
//////////////////////
|
||||
// HELPER FUNCTIONS //
|
||||
//////////////////////
|
||||
|
||||
function GetBooleanParam(paramName, defaultValue) {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const paramValue = urlParams.get(paramName);
|
||||
|
||||
if (paramValue === null) {
|
||||
return defaultValue; // Parameter not found
|
||||
}
|
||||
|
||||
const lowercaseValue = paramValue.toLowerCase(); // Handle case-insensitivity
|
||||
|
||||
if (lowercaseValue === 'true') {
|
||||
return true;
|
||||
} else if (lowercaseValue === 'false') {
|
||||
return false;
|
||||
} else {
|
||||
return paramValue; // Return original string if not 'true' or 'false'
|
||||
}
|
||||
}
|
||||
|
||||
function GetIntParam(paramName) {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const paramValue = urlParams.get(paramName);
|
||||
|
||||
if (paramValue === null) {
|
||||
return null; // or undefined, or a default value, depending on your needs
|
||||
}
|
||||
|
||||
const intValue = parseInt(paramValue, 10); // Parse as base 10 integer
|
||||
|
||||
if (isNaN(intValue)) {
|
||||
return null; // or handle the error in another way, e.g., throw an error
|
||||
}
|
||||
|
||||
return intValue;
|
||||
}
|
||||
|
||||
function GetCurrentTimeFormatted() {
|
||||
const now = new Date();
|
||||
let hours = now.getHours();
|
||||
const minutes = String(now.getMinutes()).padStart(2, '0');
|
||||
const ampm = hours >= 12 ? 'PM' : 'AM';
|
||||
|
||||
hours = hours % 12;
|
||||
hours = hours ? hours : 12; // the hour '0' should be '12'
|
||||
|
||||
const formattedTime = `${hours}:${minutes} ${ampm}`;
|
||||
return formattedTime;
|
||||
}
|
||||
|
||||
async function GetAvatar(username) {
|
||||
if (avatarMap.has(username)) {
|
||||
console.debug(`Avatar found for ${username}. Retrieving from hash map.`)
|
||||
return avatarMap.get(username);
|
||||
}
|
||||
else {
|
||||
console.debug(`No avatar found for ${username}. Retrieving from Decapi.`)
|
||||
let response = await fetch('https://decapi.me/twitch/avatar/' + username);
|
||||
let data = await response.text()
|
||||
avatarMap.set(username, data);
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
async function GetPronouns(platform, username) {
|
||||
const response = await client.getUserPronouns(platform, username);
|
||||
const userFound = response.pronoun.userFound;
|
||||
const pronouns = `${response.pronoun.pronounSubject}/${response.pronoun.pronounObject}`;
|
||||
|
||||
if (userFound)
|
||||
return `${response.pronoun.pronounSubject}/${response.pronoun.pronounObject}`;
|
||||
else
|
||||
return '';
|
||||
}
|
||||
|
||||
function AddMessageItem(element, elementID, platform, userId) {
|
||||
// Calculate the height of the div before inserting
|
||||
const tempDiv = document.getElementById('IPutThisHereSoICanCalculateHowBigEachMessageIsSupposedToBeBeforeIAddItToTheMessageList');
|
||||
tempDiv.appendChild(element);
|
||||
|
||||
setTimeout(function () {
|
||||
const calculatedWidth = tempDiv.offsetWidth + "px";
|
||||
//console.log(calculatedWidth);
|
||||
|
||||
// Create a new line item to add to the message list later
|
||||
var lineItem = document.createElement('li');
|
||||
lineItem.id = elementID;
|
||||
lineItem.dataset.platform = platform;
|
||||
lineItem.dataset.userId = userId;
|
||||
|
||||
// Move the element from the temp div to the new line item
|
||||
while (tempDiv.firstChild) {
|
||||
lineItem.appendChild(tempDiv.firstChild);
|
||||
}
|
||||
|
||||
// Add the line item to the list and animate it
|
||||
// We need to manually set the height as straight CSS can't animate on "height: auto"
|
||||
messageList.appendChild(lineItem);
|
||||
setTimeout(function () {
|
||||
lineItem.className = lineItem.className + " show";
|
||||
lineItem.style.width = calculatedWidth;
|
||||
}, 10);
|
||||
|
||||
// Remove old messages that have gone off screen to save memory
|
||||
while (messageList.clientWidth > 10 * window.innerWidth) {
|
||||
messageList.removeChild(messageList.firstChild);
|
||||
}
|
||||
|
||||
tempDiv.innerHTML = '';
|
||||
|
||||
if (hideAfter > 0)
|
||||
{
|
||||
setTimeout(function () {
|
||||
lineItem.style.opacity = 0;
|
||||
setTimeout(function() {
|
||||
messageList.removeChild(lineItem);
|
||||
}, 1000);
|
||||
}, hideAfter * 1000);
|
||||
}
|
||||
|
||||
}, 200);
|
||||
}
|
||||
|
||||
// I used Gemini for this shit so if it doesn't work, blame Google
|
||||
function FindFirstImageUrl(jsonObject) {
|
||||
if (typeof jsonObject !== 'object' || jsonObject === null) {
|
||||
return null; // Handle invalid input
|
||||
}
|
||||
|
||||
function iterate(obj) {
|
||||
if (Array.isArray(obj)) {
|
||||
for (const item of obj) {
|
||||
const result = iterate(item);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
for (const key in obj) {
|
||||
if (obj.hasOwnProperty(key)) {
|
||||
if (key === 'imageUrl') {
|
||||
return obj[key]; // Found it! Return the value.
|
||||
}
|
||||
|
||||
if (typeof obj[key] === 'object' && obj[key] !== null) {
|
||||
const result = iterate(obj[key]); // Recursive call for nested objects
|
||||
if (result) {
|
||||
return result; // Propagate the found value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null; // Key not found in this level
|
||||
}
|
||||
|
||||
return iterate(jsonObject);
|
||||
}
|
||||
|
||||
function ShowAlert(message, background = null, duration = animationDuration) {
|
||||
|
||||
// Check if the widget is in the middle of an animation
|
||||
// If any alerts are requested while the animation is playing, it should be added to the alert queue
|
||||
if (widgetLocked) {
|
||||
console.debug("Animation is progress, added alert to queue");
|
||||
let data = { message: message, background: background, duration: duration };
|
||||
alertQueue.push(data);
|
||||
return;
|
||||
}
|
||||
|
||||
// Get divs
|
||||
const messageListDiv = document.querySelector("#messageList");
|
||||
const alertBoxDiv = document.querySelector("#alertBox");
|
||||
|
||||
// Set the message text
|
||||
alertBoxDiv.innerHTML = message;
|
||||
|
||||
// Set the background
|
||||
alertBoxDiv.classList.add(background);
|
||||
|
||||
// Start the animation
|
||||
widgetLocked = true;
|
||||
messageListDiv.style.animation = 'hideAlertBox 0.5s ease-in-out forwards';
|
||||
alertBoxDiv.style.animation = 'showAlertBox 0.5s ease-in-out forwards';
|
||||
|
||||
// To stop the animation (remove the animation property):
|
||||
setTimeout(() => {
|
||||
messageListDiv.style.animation = 'showAlertBox 0.5s ease-in-out forwards';
|
||||
alertBoxDiv.style.animation = 'hideAlertBox 0.5s ease-in-out forwards';
|
||||
setTimeout(() => {
|
||||
alertBoxDiv.classList = '';
|
||||
widgetLocked = false;
|
||||
if (alertQueue.length > 0) {
|
||||
console.debug("Pulling next alert from the queue");
|
||||
let data = alertQueue.shift();
|
||||
ShowAlert(data.message, data.background, data.duration);
|
||||
}
|
||||
}, 500);
|
||||
}, duration); // Remove after 5 seconds
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////////////
|
||||
// STREAMER.BOT WEBSOCKET STATUS //
|
||||
///////////////////////////////////
|
||||
|
||||
// This function sets the visibility of the Streamer.bot status label on the overlay
|
||||
function SetConnectionStatus(connected) {
|
||||
let statusContainer = document.getElementById("statusContainer");
|
||||
if (connected) {
|
||||
statusContainer.style.background = "#2FB774";
|
||||
statusContainer.innerText = "Connected!";
|
||||
statusContainer.style.opacity = 1;
|
||||
setTimeout(() => {
|
||||
statusContainer.style.transition = "all 2s ease";
|
||||
statusContainer.style.opacity = 0;
|
||||
}, 10);
|
||||
}
|
||||
else {
|
||||
statusContainer.style.background = "#D12025";
|
||||
statusContainer.innerText = "Connecting...";
|
||||
statusContainer.style.transition = "";
|
||||
statusContainer.style.opacity = 1;
|
||||
}
|
||||
}
|
||||
|
||||
// let data = {
|
||||
// "isAnonymous": false,
|
||||
// "gifts": 10,
|
||||
// "totalGifts": 0,
|
||||
// "subTier": 1, /* 0 - Prime, 1 - Tier 1, 2 - Tier 2, 3 - Tier 3 */
|
||||
// "userName": "<username of gifter>",
|
||||
// "displayName": "<displayname of gifter>",
|
||||
// "role": 1 /* 1 - Viewer, 2 - VIP, 3 - Moderator, 4 - Broadcaster */
|
||||
// }
|
||||
|
||||
// TwitchGiftBomb(data);
|
||||
@@ -0,0 +1,64 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Settings</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
/* Remove default body margin */
|
||||
display: flex;
|
||||
/* Use flexbox for layout */
|
||||
background: #181818;
|
||||
}
|
||||
|
||||
#settings-container {
|
||||
width: 600px;
|
||||
/* 50% of viewport width */
|
||||
min-width: 600px;
|
||||
/* 50% of viewport width */
|
||||
height: 100vh;
|
||||
/* 100% of viewport height */
|
||||
border: none;
|
||||
/* Remove default iframe border */
|
||||
}
|
||||
|
||||
#widget-container {
|
||||
flex-grow: 1;
|
||||
/* Allow the content-container to take up remaining space */
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
iframe {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.content-container {
|
||||
width: 50vw;
|
||||
height: 100vh;
|
||||
background-color: #f0f0f0;
|
||||
/* Example background for right side */
|
||||
padding: 20px;
|
||||
box-sizing: border-box;
|
||||
/* Include padding in width/height */
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<iframe id="settings-container"></iframe>
|
||||
|
||||
<div id="widget-container">
|
||||
<iframe id="widget"></iframe>
|
||||
</div>
|
||||
|
||||
<script src="script.js"></script>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,29 @@
|
||||
let settingsContainer = document.getElementById('settings-container');
|
||||
settingsContainer.src = `https://nuttylmao.github.io/widget-customizer?settingsJson=${window.location.href}/settings.json`
|
||||
console.log(settingsContainer.src);
|
||||
|
||||
function reloadWidget(data) {
|
||||
let widget = document.getElementById("widget");
|
||||
widget.src = `${getParentUrl()}?${data}`;
|
||||
}
|
||||
|
||||
function getParentUrl() {
|
||||
const currentUrl = window.location.href;
|
||||
const urlParts = currentUrl.split('/');
|
||||
|
||||
// Remove the last part of the URL (the current page/file)
|
||||
urlParts.pop();
|
||||
|
||||
// Remove the last part again to go one directory up
|
||||
urlParts.pop();
|
||||
|
||||
// Reconstruct the URL
|
||||
const parentUrl = urlParts.join('/');
|
||||
|
||||
// Ensure there's a trailing slash if necessary (if it was a directory)
|
||||
if (urlParts.length > 2 && !parentUrl.endsWith('/')) {
|
||||
return parentUrl + '/';
|
||||
}
|
||||
|
||||
return parentUrl;
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
{
|
||||
"settings": [
|
||||
{
|
||||
"id": "showPlatform",
|
||||
"label": "Show Platform",
|
||||
"description": "",
|
||||
"type": "checkbox",
|
||||
"defaultValue": true,
|
||||
"group": "Appearance"
|
||||
},
|
||||
{
|
||||
"id": "showAvatar",
|
||||
"label": "Show Avatar",
|
||||
"description": "",
|
||||
"type": "checkbox",
|
||||
"defaultValue": true,
|
||||
"group": "Appearance"
|
||||
},
|
||||
{
|
||||
"id": "showTimestamps",
|
||||
"label": "Show Timestamps",
|
||||
"description": "",
|
||||
"type": "checkbox",
|
||||
"defaultValue": false,
|
||||
"group": "Appearance"
|
||||
},
|
||||
{
|
||||
"id": "showBadges",
|
||||
"label": "Show Badges",
|
||||
"description": "",
|
||||
"type": "checkbox",
|
||||
"defaultValue": true,
|
||||
"group": "Appearance"
|
||||
},
|
||||
{
|
||||
"id": "showPronouns",
|
||||
"label": "Show Pronouns",
|
||||
"description": "",
|
||||
"type": "checkbox",
|
||||
"defaultValue": false,
|
||||
"group": "Appearance"
|
||||
},
|
||||
{
|
||||
"id": "showUsername",
|
||||
"label": "Show Username",
|
||||
"description": "",
|
||||
"type": "checkbox",
|
||||
"defaultValue": true,
|
||||
"group": "Appearance"
|
||||
},
|
||||
{
|
||||
"id": "showMessage",
|
||||
"label": "Show Message",
|
||||
"description": "Honestly, if you turn this off, you're stupid lmao",
|
||||
"type": "checkbox",
|
||||
"defaultValue": true,
|
||||
"group": "Appearance"
|
||||
},
|
||||
{
|
||||
"id": "font",
|
||||
"label": "Font",
|
||||
"description": "",
|
||||
"type": "text",
|
||||
"defaultValue": "",
|
||||
"group": "Appearance"
|
||||
},
|
||||
{
|
||||
"id": "fontSize",
|
||||
"label": "Font Size",
|
||||
"description": "",
|
||||
"type": "number",
|
||||
"min": 1,
|
||||
"max": 120,
|
||||
"defaultValue": 18,
|
||||
"group": "Appearance"
|
||||
},
|
||||
{
|
||||
"id": "hideAfter",
|
||||
"label": "Hide After",
|
||||
"description": "Messages fade after X seconds (0 to disable)",
|
||||
"type": "number",
|
||||
"min": 0,
|
||||
"max": 120,
|
||||
"defaultValue": 0,
|
||||
"group": "General"
|
||||
},
|
||||
{
|
||||
"id": "excludeCommands",
|
||||
"label": "Exclude Commands",
|
||||
"description": "Hide messages starting with \"!\"",
|
||||
"type": "checkbox",
|
||||
"defaultValue": true,
|
||||
"group": "General"
|
||||
},
|
||||
{
|
||||
"id": "ignoreChatters",
|
||||
"label": "Ignore Chatters",
|
||||
"description": "Ignored chatters will not be shown",
|
||||
"type": "text",
|
||||
"defaultValue": "StreamElements,Streamlabs",
|
||||
"group": "General"
|
||||
},
|
||||
{
|
||||
"id": "showTwitchMessages",
|
||||
"label": "Chat Messages",
|
||||
"description": "",
|
||||
"type": "checkbox",
|
||||
"defaultValue": true,
|
||||
"group": "Which Twitch messages do you want to see?"
|
||||
},
|
||||
{
|
||||
"id": "showTwitchAnnouncements",
|
||||
"label": "Announcements",
|
||||
"description": "",
|
||||
"type": "checkbox",
|
||||
"defaultValue": true,
|
||||
"group": "Which Twitch messages do you want to see?"
|
||||
},
|
||||
{
|
||||
"id": "showTwitchSubs",
|
||||
"label": "New Subscribers",
|
||||
"description": "",
|
||||
"type": "checkbox",
|
||||
"defaultValue": true,
|
||||
"group": "Which Twitch messages do you want to see?"
|
||||
},
|
||||
{
|
||||
"id": "showTwitchRaids",
|
||||
"label": "Raids",
|
||||
"description": "",
|
||||
"type": "checkbox",
|
||||
"defaultValue": true,
|
||||
"group": "Which Twitch messages do you want to see?"
|
||||
},
|
||||
{
|
||||
"id": "showYouTubeMessages",
|
||||
"label": "Chat Messages",
|
||||
"description": "",
|
||||
"type": "checkbox",
|
||||
"defaultValue": true,
|
||||
"group": "Which YouTube messages do you want to see?"
|
||||
},
|
||||
{
|
||||
"id": "showYouTubeSuperChats",
|
||||
"label": "Super Chats",
|
||||
"description": "",
|
||||
"type": "checkbox",
|
||||
"defaultValue": true,
|
||||
"group": "Which YouTube messages do you want to see?"
|
||||
},
|
||||
{
|
||||
"id": "showYouTubeSuperStickers",
|
||||
"label": "Super Stickers",
|
||||
"description": "",
|
||||
"type": "checkbox",
|
||||
"defaultValue": true,
|
||||
"group": "Which YouTube messages do you want to see?"
|
||||
},
|
||||
{
|
||||
"id": "showYouTubeMemberships",
|
||||
"label": "Memberships",
|
||||
"description": "",
|
||||
"type": "checkbox",
|
||||
"defaultValue": true,
|
||||
"group": "Which YouTube messages do you want to see?"
|
||||
},
|
||||
{
|
||||
"id": "showStreamlabsDonations",
|
||||
"label": "Streamlabs Tips",
|
||||
"description": "",
|
||||
"type": "checkbox",
|
||||
"defaultValue": true,
|
||||
"group": "Which donation messages do you want to see?"
|
||||
},
|
||||
{
|
||||
"id": "showStreamElementsTips",
|
||||
"label": "StreamElements Tips",
|
||||
"description": "",
|
||||
"type": "checkbox",
|
||||
"defaultValue": true,
|
||||
"group": "Which donation messages do you want to see?"
|
||||
},
|
||||
{
|
||||
"id": "address",
|
||||
"label": "Address",
|
||||
"description": "Streamer.bot Websocket Server Address",
|
||||
"type": "text",
|
||||
"defaultValue": "127.0.0.1",
|
||||
"group": "Advanced"
|
||||
},
|
||||
{
|
||||
"id": "port",
|
||||
"label": "Port",
|
||||
"description": "Streamer.bot Websocket Server Port",
|
||||
"type": "text",
|
||||
"defaultValue": "8080",
|
||||
"group": "Advanced"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,303 @@
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
--margin: 0px;
|
||||
--border-radius: 20px;
|
||||
}
|
||||
|
||||
html {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
scroll-behavior: smooth;
|
||||
/* background: rgba(0, 0, 0, 0.884); */
|
||||
font-size: 18px;
|
||||
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica, Arial, sans-serif;
|
||||
color: white;
|
||||
text-shadow: 2px 2px 5px rgba(0, 0, 0, 0.5);
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
#statusContainer {
|
||||
font-weight: 500;
|
||||
font-size: 30px;
|
||||
text-align: center;
|
||||
background-color: #D12025;
|
||||
color: white;
|
||||
padding: 10px;
|
||||
border-radius: 10px;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
#mainContainer {
|
||||
position: absolute;
|
||||
height: calc(100% - 2 * var(--margin));
|
||||
width: calc(100% - 2 * var(--margin));
|
||||
margin: var(--margin);
|
||||
}
|
||||
|
||||
#messageList {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: nowrap;
|
||||
text-justify: right;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
#alertBox {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 50%;
|
||||
transform: translateX(50%);
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
|
||||
font-weight: 700;
|
||||
line-height: 1.7em;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
bottom: calc(-1 * var(--margin) - 2em);
|
||||
}
|
||||
|
||||
@keyframes showAlertBox {
|
||||
0% {
|
||||
bottom: calc(-1 * var(--margin) - 2em);
|
||||
}
|
||||
100% {
|
||||
bottom: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes hideAlertBox {
|
||||
0% {
|
||||
bottom: 0px;
|
||||
}
|
||||
100% {
|
||||
bottom: calc(-1 * var(--margin) - 2em);
|
||||
}
|
||||
}
|
||||
|
||||
li {
|
||||
list-style: none;
|
||||
overflow: hidden;
|
||||
width: 0px;
|
||||
transition: all 1s ease-in-out;
|
||||
}
|
||||
|
||||
.slide-fade li {
|
||||
transition: all 1s ease-out;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.slide-fade li.show {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
#IPutThisHereSoICanCalculateHowBigEachMessageIsSupposedToBeBeforeIAddItToTheMessageList {
|
||||
/* margin-top: 10px; */
|
||||
background: #2FB774;
|
||||
display: inline;
|
||||
opacity: 0;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
height: 1em;
|
||||
margin: 1px;
|
||||
transform: translate(0px, 0.25em);
|
||||
border-radius: 50%
|
||||
}
|
||||
|
||||
.timestamp,
|
||||
.pronouns {
|
||||
background: #adadad46;
|
||||
/* border-radius: var(--border-radius); */
|
||||
border-radius: 1em;
|
||||
font-size: 0.7em;
|
||||
/* padding: 5px 15px; */
|
||||
padding: 0.15em 0.5em;
|
||||
|
||||
}
|
||||
|
||||
.bits {
|
||||
background: #adadad46;
|
||||
border-radius: calc(var(--border-radius) / 2);
|
||||
font-size: 0.7em;
|
||||
padding: 5px 10px;
|
||||
}
|
||||
|
||||
.platform,
|
||||
.badge,
|
||||
.emote {
|
||||
height: 1em;
|
||||
margin: 1px;
|
||||
transform: translate(0px, 0.25em);
|
||||
}
|
||||
|
||||
#badgeList {
|
||||
margin: 0px 5px;
|
||||
}
|
||||
|
||||
#username,
|
||||
#title {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
#message {
|
||||
font-weight: 700;
|
||||
line-height: 1.7em;
|
||||
margin: 0px 5px;
|
||||
}
|
||||
|
||||
/* .firstMessage {
|
||||
display: none;
|
||||
font-size: 0.7em;
|
||||
} */
|
||||
|
||||
/* .firstMessageHighlight {
|
||||
background: #adadad46;
|
||||
padding: 20px 40px;
|
||||
border-radius: var(--border-radius);
|
||||
} */
|
||||
|
||||
/* .reply {
|
||||
display: none;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
font-size: 0.7em;
|
||||
opacity: 0.5;
|
||||
} */
|
||||
|
||||
/* .card {
|
||||
background: #adadad46;
|
||||
padding: 20px 40px;
|
||||
border-radius: var(--border-radius);
|
||||
} */
|
||||
|
||||
/* .youtube-super-sticker {
|
||||
display: block;
|
||||
} */
|
||||
|
||||
/* .sticker-img {
|
||||
height: 5em;
|
||||
} */
|
||||
|
||||
/*****************/
|
||||
/** CARD COLORS **/
|
||||
/*****************/
|
||||
|
||||
.announcementBlue {
|
||||
background: linear-gradient(#03d3d7BF, #8d49feBF) !important;
|
||||
}
|
||||
|
||||
.announcementGreen {
|
||||
background: linear-gradient(#01da86BF, #55bee4BF) !important;
|
||||
}
|
||||
|
||||
.announcementOrange {
|
||||
background: linear-gradient(#feb419BF, #e1df00BF) !important;
|
||||
}
|
||||
|
||||
.announcementPurple {
|
||||
background: linear-gradient(#9548ffBF, #fc74e6BF) !important;
|
||||
}
|
||||
|
||||
.twitch {
|
||||
background: linear-gradient(#6d5ca1bf, #9146ffBF) !important;
|
||||
}
|
||||
|
||||
.youtube {
|
||||
background: linear-gradient(#FF6C60BF, #FF0707BF) !important;
|
||||
}
|
||||
|
||||
.streamlabs {
|
||||
background: linear-gradient(#73dabbbf, #397765bf) !important;
|
||||
}
|
||||
|
||||
.streamelements {
|
||||
background: linear-gradient(#263b8abf, #0a112abf) !important;
|
||||
}
|
||||
|
||||
/****************/
|
||||
/** ANIMATIONS **/
|
||||
/****************/
|
||||
|
||||
.statusConnected {
|
||||
animation-name: statusConnected;
|
||||
animation-duration: 2s;
|
||||
animation-fill-mode: forwards;
|
||||
}
|
||||
|
||||
.statusDisconnected {
|
||||
animation-name: statusDisconnected;
|
||||
animation-duration: 0.5s;
|
||||
animation-fill-mode: forwards;
|
||||
}
|
||||
|
||||
@keyframes statusConnected {
|
||||
0% {
|
||||
opacity: 1;
|
||||
background-color: #2FB774;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 0;
|
||||
background-color: #2FB774;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes statusDisconnected {
|
||||
0% {
|
||||
background-color: #D12025;
|
||||
}
|
||||
|
||||
100% {
|
||||
background-color: #D12025;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
#message {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#username {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#colon-separator {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#avatar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#platform {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#badgeList {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#timestamp {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#pronouns {
|
||||
display: none;
|
||||
}
|
||||
*/
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 24 24" width="20" height="20" aria-label="Icon" role="img"><path fill-rule="evenodd" d="M18 6a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-1.959l2.75 1.588A1.5 1.5 0 0 0 23 16.33V7.67a1.5 1.5 0 0 0-2.25-1.3L18 7.96z" clip-rule="evenodd"></path></svg>
|
||||
|
After Width: | Height: | Size: 343 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><!-- Icon from Material Design Icons by Pictogrammers - https://github.com/Templarian/MaterialDesign/blob/master/LICENSE --><path fill="currentColor" d="M12 17.27L18.18 21l-1.64-7.03L22 9.24l-7.19-.62L12 2L9.19 8.62L2 9.24l5.45 4.73L5.82 21z"/></svg>
|
||||
|
After Width: | Height: | Size: 333 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><!-- Icon from Material Design Icons by Pictogrammers - https://github.com/Templarian/MaterialDesign/blob/master/LICENSE --><path fill="currentColor" d="M6.92 5H5l9 9l1-.94m4.96 6.06l-.84.84a.996.996 0 0 1-1.41 0l-3.12-3.12l-2.68 2.66l-1.41-1.41l1.42-1.42L3 7.75V3h4.75l8.92 8.92l1.42-1.42l1.41 1.41l-2.67 2.67l3.12 3.12c.4.4.4 1.03.01 1.42"/></svg>
|
||||
|
After Width: | Height: | Size: 432 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><!-- Icon from All by undefined - undefined --><path fill="currentColor" fill-rule="evenodd" d="M9.592 3.2a6 6 0 0 1-.495.399c-.298.2-.633.338-.985.408c-.153.03-.313.043-.632.068c-.801.064-1.202.096-1.536.214a2.71 2.71 0 0 0-1.655 1.655c-.118.334-.15.735-.214 1.536a6 6 0 0 1-.068.632c-.07.352-.208.687-.408.985c-.087.13-.191.252-.399.495c-.521.612-.782.918-.935 1.238c-.353.74-.353 1.6 0 2.34c.153.32.414.626.935 1.238c.208.243.312.365.399.495c.2.298.338.633.408.985c.03.153.043.313.068.632c.064.801.096 1.202.214 1.536a2.71 2.71 0 0 0 1.655 1.655c.334.118.735.15 1.536.214c.319.025.479.038.632.068c.352.07.687.209.985.408c.13.087.252.191.495.399c.612.521.918.782 1.238.935c.74.353 1.6.353 2.34 0c.32-.153.626-.414 1.238-.935c.243-.208.365-.312.495-.399c.298-.2.633-.338.985-.408c.153-.03.313-.043.632-.068c.801-.064 1.202-.096 1.536-.214a2.71 2.71 0 0 0 1.655-1.655c.118-.334.15-.735.214-1.536c.025-.319.038-.479.068-.632c.07-.352.209-.687.408-.985c.087-.13.191-.252.399-.495c.521-.612.782-.918.935-1.238c.353-.74.353-1.6 0-2.34c-.153-.32-.414-.626-.935-1.238a6 6 0 0 1-.399-.495a2.7 2.7 0 0 1-.408-.985a6 6 0 0 1-.068-.632c-.064-.801-.096-1.202-.214-1.536a2.71 2.71 0 0 0-1.655-1.655c-.334-.118-.735-.15-1.536-.214a6 6 0 0 1-.632-.068a2.7 2.7 0 0 1-.985-.408a6 6 0 0 1-.495-.399c-.612-.521-.918-.782-1.238-.935a2.71 2.71 0 0 0-2.34 0c-.32.153-.626.414-1.238.935m6.781 6.663a.814.814 0 0 0-1.15-1.15l-4.85 4.85l-1.596-1.595a.814.814 0 0 0-1.15 1.15l2.17 2.17a.814.814 0 0 0 1.15 0z" clip-rule="evenodd"/></svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 8.4 KiB |
@@ -0,0 +1,70 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="shortcut icon" href="#" />
|
||||
<link rel="stylesheet" href="./style.css" />
|
||||
<script type="text/javascript" src="https://unpkg.com/@streamerbot/client/dist/streamerbot-client.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="statusContainer">Connecting...</div>
|
||||
<div id="mainContainer" class="slide-fade">
|
||||
<ul id="messageList">
|
||||
</ul>
|
||||
<div id="IPutThisHereSoICanCalculateHowBigEachMessageIsSupposedToBeBeforeIAddItToTheMessageList">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<template id="messageTemplate">
|
||||
<div id="messageContainer">
|
||||
<label>
|
||||
<div id="firstMessage">
|
||||
<span>✨ First Time Chat</span>
|
||||
<span> from viewer</span>
|
||||
</div>
|
||||
<label id="userInfo">
|
||||
<span id="avatar"></span>
|
||||
<span id="timestamp"></span>
|
||||
<span id="platform"></span>
|
||||
<span id="badgeList"></span>
|
||||
<span id="pronouns"></span>
|
||||
<span id="username"></span>
|
||||
<span id="colon-separator" style="display: none;">: </span>
|
||||
<br id="line-space">
|
||||
</label>
|
||||
<div id="reply">
|
||||
<span>➥ Replying to <span id="replyUser"></span>: <span id="replyMsg"></span></span>
|
||||
</div>
|
||||
<span id="message"></span>
|
||||
</label>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template id="cardTemplate">
|
||||
<div id="card">
|
||||
<label id="header">
|
||||
<span id="avatar"></span>
|
||||
<span id="icon"></span>
|
||||
<span id="title"></span>
|
||||
<br id="line-space">
|
||||
</label>
|
||||
<span id="content"></span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template id="stickerTemplate">
|
||||
<div id="sticker">
|
||||
<div id="youtubeSuperSticker">
|
||||
<div style="width: 100%; text-align: center;">
|
||||
<img id="stickerImg">
|
||||
</div>
|
||||
<div id="stickerLabel"></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</body>
|
||||
|
||||
<script src="./script.js"></script>
|
||||
@@ -0,0 +1,64 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Settings</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
/* Remove default body margin */
|
||||
display: flex;
|
||||
/* Use flexbox for layout */
|
||||
background: #181818;
|
||||
}
|
||||
|
||||
#settings-container {
|
||||
width: 600px;
|
||||
/* 50% of viewport width */
|
||||
min-width: 600px;
|
||||
/* 50% of viewport width */
|
||||
height: 100vh;
|
||||
/* 100% of viewport height */
|
||||
border: none;
|
||||
/* Remove default iframe border */
|
||||
}
|
||||
|
||||
#widget-container {
|
||||
flex-grow: 1;
|
||||
/* Allow the content-container to take up remaining space */
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
iframe {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.content-container {
|
||||
width: 50vw;
|
||||
height: 100vh;
|
||||
background-color: #f0f0f0;
|
||||
/* Example background for right side */
|
||||
padding: 20px;
|
||||
box-sizing: border-box;
|
||||
/* Include padding in width/height */
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<iframe id="settings-container"></iframe>
|
||||
|
||||
<div id="widget-container">
|
||||
<iframe id="widget"></iframe>
|
||||
</div>
|
||||
|
||||
<script src="script.js"></script>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,29 @@
|
||||
let settingsContainer = document.getElementById('settings-container');
|
||||
settingsContainer.src = `https://nuttylmao.github.io/widget-customizer?settingsJson=${window.location.href}/settings.json`
|
||||
console.log(settingsContainer.src);
|
||||
|
||||
function reloadWidget(data) {
|
||||
let widget = document.getElementById("widget");
|
||||
widget.src = `${getParentUrl()}?${data}`;
|
||||
}
|
||||
|
||||
function getParentUrl() {
|
||||
const currentUrl = window.location.href;
|
||||
const urlParts = currentUrl.split('/');
|
||||
|
||||
// Remove the last part of the URL (the current page/file)
|
||||
urlParts.pop();
|
||||
|
||||
// Remove the last part again to go one directory up
|
||||
urlParts.pop();
|
||||
|
||||
// Reconstruct the URL
|
||||
const parentUrl = urlParts.join('/');
|
||||
|
||||
// Ensure there's a trailing slash if necessary (if it was a directory)
|
||||
if (urlParts.length > 2 && !parentUrl.endsWith('/')) {
|
||||
return parentUrl + '/';
|
||||
}
|
||||
|
||||
return parentUrl;
|
||||
}
|
||||
@@ -0,0 +1,271 @@
|
||||
{
|
||||
"settings": [
|
||||
{
|
||||
"id": "showPlatform",
|
||||
"label": "Show Platform",
|
||||
"description": "",
|
||||
"type": "checkbox",
|
||||
"defaultValue": true,
|
||||
"group": "Appearance"
|
||||
},
|
||||
{
|
||||
"id": "showAvatar",
|
||||
"label": "Show Avatar",
|
||||
"description": "",
|
||||
"type": "checkbox",
|
||||
"defaultValue": true,
|
||||
"group": "Appearance"
|
||||
},
|
||||
{
|
||||
"id": "showTimestamps",
|
||||
"label": "Show Timestamps",
|
||||
"description": "",
|
||||
"type": "checkbox",
|
||||
"defaultValue": true,
|
||||
"group": "Appearance"
|
||||
},
|
||||
{
|
||||
"id": "showBadges",
|
||||
"label": "Show Badges",
|
||||
"description": "",
|
||||
"type": "checkbox",
|
||||
"defaultValue": true,
|
||||
"group": "Appearance"
|
||||
},
|
||||
{
|
||||
"id": "showPronouns",
|
||||
"label": "Show Pronouns",
|
||||
"description": "",
|
||||
"type": "checkbox",
|
||||
"defaultValue": true,
|
||||
"group": "Appearance"
|
||||
},
|
||||
{
|
||||
"id": "showUsername",
|
||||
"label": "Show Username",
|
||||
"description": "",
|
||||
"type": "checkbox",
|
||||
"defaultValue": true,
|
||||
"group": "Appearance"
|
||||
},
|
||||
{
|
||||
"id": "showMessage",
|
||||
"label": "Show Message",
|
||||
"description": "Honestly, if you turn this off, you're stupid lmao",
|
||||
"type": "checkbox",
|
||||
"defaultValue": true,
|
||||
"group": "Appearance"
|
||||
},
|
||||
{
|
||||
"id": "font",
|
||||
"label": "Font",
|
||||
"description": "",
|
||||
"type": "text",
|
||||
"defaultValue": "",
|
||||
"group": "Appearance"
|
||||
},
|
||||
{
|
||||
"id": "fontSize",
|
||||
"label": "Font Size",
|
||||
"description": "",
|
||||
"type": "number",
|
||||
"min": 1,
|
||||
"max": 120,
|
||||
"defaultValue": 30,
|
||||
"group": "Appearance"
|
||||
},
|
||||
{
|
||||
"id": "background",
|
||||
"label": "Background",
|
||||
"description": "",
|
||||
"type": "color",
|
||||
"defaultValue": "#000000",
|
||||
"group": "Appearance"
|
||||
},
|
||||
{
|
||||
"id": "opacity",
|
||||
"label": "Background Opacity",
|
||||
"description": "",
|
||||
"type": "number",
|
||||
"defaultValue": "0.85",
|
||||
"min": 0,
|
||||
"max": 1,
|
||||
"step": ".01",
|
||||
"group": "Appearance"
|
||||
},
|
||||
{
|
||||
"id": "hideAfter",
|
||||
"label": "Hide After",
|
||||
"description": "Messages fade after X seconds (0 to disable)",
|
||||
"type": "number",
|
||||
"min": 0,
|
||||
"max": 120,
|
||||
"defaultValue": 0,
|
||||
"group": "General"
|
||||
},
|
||||
{
|
||||
"id": "excludeCommands",
|
||||
"label": "Exclude Commands",
|
||||
"description": "Hide messages starting with \"!\"",
|
||||
"type": "checkbox",
|
||||
"defaultValue": true,
|
||||
"group": "General"
|
||||
},
|
||||
{
|
||||
"id": "ignoreChatters",
|
||||
"label": "Ignore Chatters",
|
||||
"description": "Ignored chatters will not be shown",
|
||||
"type": "text",
|
||||
"defaultValue": "StreamElements,Streamlabs",
|
||||
"group": "General"
|
||||
},
|
||||
{
|
||||
"id": "scrollDirection",
|
||||
"label": "Scroll Direction",
|
||||
"description": "",
|
||||
"type": "select",
|
||||
"options": [
|
||||
{
|
||||
"value": 1,
|
||||
"label": "Normal"
|
||||
},
|
||||
{
|
||||
"value": 2,
|
||||
"label": "Reversed"
|
||||
}
|
||||
],
|
||||
"defaultValue": 1,
|
||||
"group": "General"
|
||||
},
|
||||
{
|
||||
"id": "imageEmbedPermissionLevel",
|
||||
"label": "Embed Images",
|
||||
"description": "Automatically embed images for these users",
|
||||
"type": "select",
|
||||
"options": [
|
||||
{
|
||||
"value": 40,
|
||||
"label": "Broadcaster"
|
||||
},
|
||||
{
|
||||
"value": 30,
|
||||
"label": "Mods & Broadcaster"
|
||||
},
|
||||
{
|
||||
"value": 20,
|
||||
"label": "VIPs, Mods & Broadcaster"
|
||||
},
|
||||
{
|
||||
"value": 15,
|
||||
"label": "Subs, VIPs, Mods & Broadcaster"
|
||||
},
|
||||
{
|
||||
"value": 10,
|
||||
"label": "Everyone"
|
||||
},
|
||||
{
|
||||
"value": 69420,
|
||||
"label": "Nobody"
|
||||
}
|
||||
],
|
||||
"defaultValue": 20,
|
||||
"group": "General"
|
||||
},
|
||||
{
|
||||
"id": "showTwitchMessages",
|
||||
"label": "Chat Messages",
|
||||
"description": "",
|
||||
"type": "checkbox",
|
||||
"defaultValue": true,
|
||||
"group": "Which Twitch messages do you want to see?"
|
||||
},
|
||||
{
|
||||
"id": "showTwitchAnnouncements",
|
||||
"label": "Announcements",
|
||||
"description": "",
|
||||
"type": "checkbox",
|
||||
"defaultValue": true,
|
||||
"group": "Which Twitch messages do you want to see?"
|
||||
},
|
||||
{
|
||||
"id": "showTwitchSubs",
|
||||
"label": "New Subscribers",
|
||||
"description": "",
|
||||
"type": "checkbox",
|
||||
"defaultValue": true,
|
||||
"group": "Which Twitch messages do you want to see?"
|
||||
},
|
||||
{
|
||||
"id": "showTwitchRaids",
|
||||
"label": "Raids",
|
||||
"description": "",
|
||||
"type": "checkbox",
|
||||
"defaultValue": true,
|
||||
"group": "Which Twitch messages do you want to see?"
|
||||
},
|
||||
{
|
||||
"id": "showYouTubeMessages",
|
||||
"label": "Chat Messages",
|
||||
"description": "",
|
||||
"type": "checkbox",
|
||||
"defaultValue": true,
|
||||
"group": "Which YouTube messages do you want to see?"
|
||||
},
|
||||
{
|
||||
"id": "showYouTubeSuperChats",
|
||||
"label": "Super Chats",
|
||||
"description": "",
|
||||
"type": "checkbox",
|
||||
"defaultValue": true,
|
||||
"group": "Which YouTube messages do you want to see?"
|
||||
},
|
||||
{
|
||||
"id": "showYouTubeSuperStickers",
|
||||
"label": "Super Stickers",
|
||||
"description": "",
|
||||
"type": "checkbox",
|
||||
"defaultValue": true,
|
||||
"group": "Which YouTube messages do you want to see?"
|
||||
},
|
||||
{
|
||||
"id": "showYouTubeMemberships",
|
||||
"label": "Memberships",
|
||||
"description": "",
|
||||
"type": "checkbox",
|
||||
"defaultValue": true,
|
||||
"group": "Which YouTube messages do you want to see?"
|
||||
},
|
||||
{
|
||||
"id": "showStreamlabsDonations",
|
||||
"label": "Streamlabs Tips",
|
||||
"description": "",
|
||||
"type": "checkbox",
|
||||
"defaultValue": true,
|
||||
"group": "Which donation messages do you want to see?"
|
||||
},
|
||||
{
|
||||
"id": "showStreamElementsTips",
|
||||
"label": "StreamElements Tips",
|
||||
"description": "",
|
||||
"type": "checkbox",
|
||||
"defaultValue": true,
|
||||
"group": "Which donation messages do you want to see?"
|
||||
},
|
||||
{
|
||||
"id": "address",
|
||||
"label": "Address",
|
||||
"description": "Streamer.bot Websocket Server Address",
|
||||
"type": "text",
|
||||
"defaultValue": "127.0.0.1",
|
||||
"group": "Advanced"
|
||||
},
|
||||
{
|
||||
"id": "port",
|
||||
"label": "Port",
|
||||
"description": "Streamer.bot Websocket Server Port",
|
||||
"type": "text",
|
||||
"defaultValue": "8080",
|
||||
"group": "Advanced"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,245 @@
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
--margin: 40px;
|
||||
--border-radius: 20px;
|
||||
}
|
||||
|
||||
html {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
scroll-behavior: smooth;
|
||||
/* background: #000000d9; */
|
||||
font-size: 30px;
|
||||
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica, Arial, sans-serif;
|
||||
color: white;
|
||||
text-shadow: 2px 2px 5px rgba(0, 0, 0, 0.5);
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
#statusContainer {
|
||||
font-weight: 500;
|
||||
font-size: 30px;
|
||||
text-align: center;
|
||||
background-color: #D12025;
|
||||
color: white;
|
||||
padding: 10px;
|
||||
border-radius: 10px;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
#mainContainer {
|
||||
position: absolute;
|
||||
height: calc(100% - 2 * var(--margin));
|
||||
width: calc(100% - 2 * var(--margin));
|
||||
margin: var(--margin);
|
||||
}
|
||||
|
||||
#messageList {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.normalScrollDirection {
|
||||
bottom: 0;
|
||||
}
|
||||
.reverseScrollDirection {
|
||||
display: flex;
|
||||
top: 0;
|
||||
flex-direction: column-reverse;
|
||||
}
|
||||
|
||||
li {
|
||||
list-style: none;
|
||||
overflow: hidden;
|
||||
height: 0;
|
||||
margin-top: 10px;
|
||||
transition: all 1s ease-in-out;
|
||||
}
|
||||
.reverseLineItemDirection {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.slide-fade li {
|
||||
transition: all 0.5s ease-out;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.slide-fade li.show {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
#IPutThisHereSoICanCalculateHowBigEachMessageIsSupposedToBeBeforeIAddItToTheMessageList {
|
||||
margin-top: 10px;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
#firstMessage {
|
||||
display: none;
|
||||
font-weight: 500;
|
||||
font-size: 0.7em;
|
||||
}
|
||||
|
||||
.firstMessageHighlight {
|
||||
background: #adadad46;
|
||||
padding: 20px 40px;
|
||||
border-radius: var(--border-radius);
|
||||
}
|
||||
|
||||
.avatar {
|
||||
height: 2em;
|
||||
margin: 1px;
|
||||
transform: translate(0px, 0.25em);
|
||||
border-radius: 50%
|
||||
}
|
||||
|
||||
.timestamp,
|
||||
.pronouns {
|
||||
background: #adadad46;
|
||||
/* border-radius: var(--border-radius); */
|
||||
border-radius: 1em;
|
||||
font-size: 0.7em;
|
||||
/* padding: 5px 15px; */
|
||||
padding: 0.15em 0.5em;
|
||||
}
|
||||
|
||||
.bits {
|
||||
background: #adadad46;
|
||||
border-radius: calc(var(--border-radius) / 2);
|
||||
font-size: 0.7em;
|
||||
padding: 5px 10px;
|
||||
}
|
||||
|
||||
.platform,
|
||||
.badge,
|
||||
.emote {
|
||||
height: 1em;
|
||||
margin: 1px;
|
||||
transform: translate(0px, 0.25em);
|
||||
}
|
||||
|
||||
#badgeList {
|
||||
margin: 0px 5px;
|
||||
}
|
||||
|
||||
#username,
|
||||
#title {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
#reply {
|
||||
display: none;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
font-size: 0.7em;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
#message {
|
||||
font-weight: 400;
|
||||
line-height: 1.7em;
|
||||
}
|
||||
|
||||
#card {
|
||||
background: #adadad46;
|
||||
padding: 20px 40px;
|
||||
border-radius: var(--border-radius);
|
||||
}
|
||||
|
||||
#youtubeSuperSticker {
|
||||
display: block;
|
||||
}
|
||||
|
||||
#sticker-img {
|
||||
height: 5em;
|
||||
}
|
||||
|
||||
#stickerLabel {
|
||||
text-align: center;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
/*****************/
|
||||
/** CARD COLORS **/
|
||||
/*****************/
|
||||
|
||||
.announcementBlue {
|
||||
background: linear-gradient(#03d3d7BF, #8d49feBF) !important;
|
||||
}
|
||||
|
||||
.announcementGreen {
|
||||
background: linear-gradient(#01da86BF, #55bee4BF) !important;
|
||||
}
|
||||
|
||||
.announcementOrange {
|
||||
background: linear-gradient(#feb419BF, #e1df00BF) !important;
|
||||
}
|
||||
|
||||
.announcementPurple {
|
||||
background: linear-gradient(#9548ffBF, #fc74e6BF) !important;
|
||||
}
|
||||
|
||||
.twitch {
|
||||
background: linear-gradient(#6d5ca1bf, #9146ffBF) !important;
|
||||
}
|
||||
|
||||
.youtube {
|
||||
background: linear-gradient(#FF6C60BF, #FF0707BF) !important;
|
||||
}
|
||||
|
||||
.streamlabs {
|
||||
background: linear-gradient(#73dabbbf, #397765bf) !important;
|
||||
}
|
||||
|
||||
.streamelements {
|
||||
background: linear-gradient(#263b8abf, #0a112abf) !important;
|
||||
}
|
||||
|
||||
/****************/
|
||||
/** ANIMATIONS **/
|
||||
/****************/
|
||||
|
||||
.statusConnected {
|
||||
animation-name: statusConnected;
|
||||
animation-duration: 2s;
|
||||
animation-fill-mode: forwards;
|
||||
}
|
||||
|
||||
.statusDisconnected {
|
||||
animation-name: statusDisconnected;
|
||||
animation-duration: 0.5s;
|
||||
animation-fill-mode: forwards;
|
||||
}
|
||||
|
||||
@keyframes statusConnected {
|
||||
0% {
|
||||
opacity: 1;
|
||||
background-color: #2FB774;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 0;
|
||||
background-color: #2FB774;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes statusDisconnected {
|
||||
0% {
|
||||
background-color: #D12025;
|
||||
}
|
||||
|
||||
100% {
|
||||
background-color: #D12025;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg width="100%" height="100%" viewBox="0 0 2000 1556" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||
<g transform="matrix(30.772,0,0,30.772,323.017,878.74)">
|
||||
<path d="M32.927,8.977L22,22L11.073,8.977C14.134,6.408 18.003,5 22,5C25.997,5 29.866,6.408 32.927,8.977ZM-5.271,-10.5L-10.497,-16.729C-1.392,-24.369 10.114,-28.557 22,-28.557C33.886,-28.557 45.392,-24.369 54.497,-16.729L49.271,-10.5C41.63,-16.912 31.974,-20.426 22,-20.426C12.026,-20.426 2.37,-16.912 -5.271,-10.5ZM4.373,0.993L0.145,-4.046C6.269,-9.184 14.007,-12 22,-12C29.993,-12 37.731,-9.184 43.855,-4.046L39.627,0.993C34.688,-3.151 28.447,-5.422 22,-5.422C15.553,-5.422 9.312,-3.151 4.373,0.993Z" style="fill:rgb(0,230,118);"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg width="100%" height="100%" viewBox="0 0 1865 1556" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||
<g transform="matrix(86.4291,0,0,86.4291,-901.44,-432.146)">
|
||||
<path d="M32,9L32,15L30,15L30,9L32,9ZM32,17L32,19L30,19L30,17L32,17ZM28.048,9.158C26.155,8.331 24.097,7.895 22,7.895C18.449,7.895 15.011,9.146 12.291,11.429L10.43,9.211C13.672,6.491 17.768,5 22,5C24.923,5 27.782,5.712 30.339,7.048L30,7.048C28.922,7.048 28.048,7.922 28.048,9L28.048,9.158ZM28.082,15.362C26.355,13.988 24.212,13.237 22,13.237C19.705,13.237 17.483,14.045 15.724,15.521L14.219,13.727C16.399,11.897 19.154,10.895 22,10.895C24.135,10.895 26.22,11.459 28.048,12.514L28.048,15C28.048,15.124 28.06,15.245 28.082,15.362ZM25.891,18.363L22,23L18.109,18.363C19.2,17.449 20.577,16.947 22,16.947C23.423,16.947 24.8,17.449 25.891,18.363Z" style="fill:rgb(221,44,0);"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -0,0 +1,81 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="shortcut icon" href="#" />
|
||||
<link rel="stylesheet" href="./style.css" />
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@xz/fonts@1/serve/metropolis.min.css" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="refreshContainer">Refreshed</div>
|
||||
|
||||
<div id="instructionsContainer">
|
||||
<div id="streamerbotConnectInstructions" style="display: none;">
|
||||
<div style="padding: 10px 20px;">
|
||||
Please connect to Streamer.bot.
|
||||
<br>
|
||||
Click <a class="hyperlinks" href="https://nuttylmao.notion.site/Multistream-Title-Updater-e2fb7b24c7514f9cbd943729f54be1d9" target="_blank">here</a> for instructions.
|
||||
</div>
|
||||
</div>
|
||||
<div id="missingActionsInstructions" style="display: none; ">
|
||||
<div style="padding: 10px 20px;">
|
||||
Streamer.bot is connected, but you are missing actions.
|
||||
Please import <label style="color: #ffb700;">multistream-title-updater.nut</label> and refresh the page.
|
||||
Click <a class="hyperlinks" href="https://nuttylmao.notion.site/Multistream-Title-Updater-e2fb7b24c7514f9cbd943729f54be1d9" target="_blank">here</a> for instructions.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div id="mainContainer">
|
||||
|
||||
<div class="straightUpIloveCum">
|
||||
<div id="title" style="font-size: 20px;">Update Titles</div>
|
||||
<button id="infoIcon" style="padding: 0px 10px;">
|
||||
<img src="info.svg" height="16px"/>
|
||||
</button>
|
||||
<img src="disconnected.svg" id="connectionStatusIcon"/>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
|
||||
<div id="ThisIsWhereAllTheCoolStuffHappens">
|
||||
<div>
|
||||
<input type="text" id="titleInput" class="textBox"><br><br>
|
||||
<div class="straightUpIloveCum">
|
||||
<input type="submit" id="submitButton" class="button submitButton" value="UPDATE ALL">
|
||||
<input type="submit" id="refreshButton" class="button" value="REFRESH">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
|
||||
<div id="fieldsContainer">
|
||||
</div>
|
||||
|
||||
|
||||
<template id="platformTemplate">
|
||||
<div class="broadcastBox">
|
||||
<button id="platformIconButton" style="padding: 0px 10px;">
|
||||
<img id="icon" height="20px" style="padding: 0px 10px 0px 0px;"/>
|
||||
</button>
|
||||
<input type="text" class="textBox platformTitle">
|
||||
<input type="submit" class="button submitButton platformSubmitButton" value="UPDATE">
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
|
||||
<div id="noYouTubeStreamsInstructions" style="display: none; text-align: center; margin: 10px; font-weight: bold; opacity: 0.5;">
|
||||
<div>
|
||||
⚠️ YouTube titles can only be updated after the stream has started ⚠️
|
||||
<br><br>
|
||||
Please start your stream to update YouTube titles
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
<script src='https://cdnjs.cloudflare.com/ajax/libs/gsap/1.18.0/TweenMax.min.js'></script>
|
||||
<script src="./script.js"></script>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg width="100%" height="100%" viewBox="0 0 2000 1556" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||
<path d="M1000,0C1429.31,0 1777.86,348.548 1777.86,777.862C1777.86,1207.18 1429.31,1555.72 1000,1555.72C570.686,1555.72 222.138,1207.18 222.138,777.862C222.138,348.548 570.686,0 1000,0ZM934,714.686L934,1145L865,1145L865,1216.69L1137.63,1216.69L1137.63,1145L1068.63,1145L1068.63,643L865,643L865,714.686L934,714.686ZM1000,305C943.705,305 898,350.705 898,407C898,463.295 943.705,509 1000,509C1056.3,509 1102,463.295 1102,407C1102,350.705 1056.3,305 1000,305Z" style="fill:white;"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 937 B |
@@ -0,0 +1,4 @@
|
||||
[NUT] Multistream Title Updater | Fetch Broadcasts
|
||||
[NUT] Multistream Title Updater | Update All Broadcasts
|
||||
[NUT] Multistream Title Updater | Update Twitch Title
|
||||
[NUT] Multistream Title Updater | Update YouTube Title
|
||||
@@ -0,0 +1,444 @@
|
||||
////////////
|
||||
// FIELDS //
|
||||
////////////
|
||||
|
||||
let sbDebugMode = true;
|
||||
|
||||
const queryString = window.location.search;
|
||||
const urlParams = new URLSearchParams(queryString);
|
||||
|
||||
const sbServerPort = urlParams.get("port") || 8080;
|
||||
const sbServerAddress = urlParams.get("server") || "127.0.0.1";
|
||||
|
||||
/////////////////
|
||||
// GLOBAL VARS //
|
||||
/////////////////
|
||||
|
||||
let ws;
|
||||
|
||||
///////////////////////////////////
|
||||
// SRTEAMER.BOT WEBSOCKET SERVER //
|
||||
///////////////////////////////////
|
||||
|
||||
// This is the main function that connects to the Streamer.bot websocke server
|
||||
function connectws() {
|
||||
if ("WebSocket" in window) {
|
||||
ws = new WebSocket("ws://" + sbServerAddress + ":" + sbServerPort + "/");
|
||||
|
||||
// Reconnect
|
||||
ws.onclose = function () {
|
||||
SetConnectionStatus(false);
|
||||
setTimeout(connectws, 5000);
|
||||
};
|
||||
|
||||
// Connect
|
||||
ws.onopen = async function () {
|
||||
SetConnectionStatus(true);
|
||||
|
||||
console.log("Subscribe to events");
|
||||
ws.send(
|
||||
JSON.stringify({
|
||||
request: "Subscribe",
|
||||
id: "subscribe-events-id",
|
||||
// This is the list of Streamer.bot websocket events to subscribe to
|
||||
// See full list of events here:
|
||||
// https://docs.streamer.bot/api/servers/websocket/requests
|
||||
events: {
|
||||
twitch: [
|
||||
"StreamUpdate"
|
||||
],
|
||||
youTube: [
|
||||
"BroadcastStarted",
|
||||
"BroadcastEnded",
|
||||
"BroadcastUpdated"
|
||||
],
|
||||
general: [
|
||||
"Custom"
|
||||
]
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
sbGetActions(ws);
|
||||
|
||||
ws.onmessage = function (event) {
|
||||
// Grab message and parse JSON
|
||||
const msg = event.data;
|
||||
const wsdata = JSON.parse(msg);
|
||||
|
||||
// Check if the user installed all the required Streamer.bot actions
|
||||
if (wsdata.id == "GetActions") {
|
||||
|
||||
// Check if all the required SB action exist
|
||||
ReadLinesFromFile('requiredActions.txt')
|
||||
.then(requiredActions => {
|
||||
if (sbCheckRequiredActions(wsdata.actions, requiredActions)) {
|
||||
SetElementVisibility("missingActionsInstructions", false);
|
||||
sbFetchBroadcasts(ws);
|
||||
}
|
||||
else
|
||||
SetElementVisibility("missingActionsInstructions", true);
|
||||
})
|
||||
}
|
||||
|
||||
if (typeof wsdata.event == "undefined") {
|
||||
return;
|
||||
}
|
||||
|
||||
// Print data to log for debugging purposes
|
||||
if (sbDebugMode) {
|
||||
console.log(wsdata.data);
|
||||
console.log(wsdata.event.source);
|
||||
console.log(wsdata.event.type);
|
||||
}
|
||||
|
||||
// Check for events to trigger
|
||||
// See documentation for all events here:
|
||||
// https://wiki.streamer.bot/en/Servers-Clients/WebSocket-Server/Events
|
||||
switch (wsdata.event.source) {
|
||||
// Twitch Events
|
||||
case 'Twitch':
|
||||
switch (wsdata.event.type) {
|
||||
case ('StreamUpdate'):
|
||||
sbFetchBroadcasts(ws);
|
||||
break;
|
||||
}
|
||||
// Twitch Events
|
||||
case 'YouTube':
|
||||
switch (wsdata.event.type) {
|
||||
case ('BroadcastStarted'):
|
||||
case ('BroadcastEnded'):
|
||||
case ('BroadcastUpdated'):
|
||||
sbFetchBroadcasts(ws);
|
||||
break;
|
||||
}
|
||||
// General Events
|
||||
case 'General':
|
||||
switch (wsdata.event.type) {
|
||||
case ('Custom'):
|
||||
switch (wsdata.data.action) {
|
||||
case ('[NUT] Multistream Title Updater | Fetch Broadcasts'):
|
||||
UpdateBroadcastList(wsdata.data);
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
break;
|
||||
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/////////////////////////
|
||||
// STREAMER.BOT WIDGET //
|
||||
/////////////////////////
|
||||
|
||||
function sbGetActions(ws) {
|
||||
|
||||
let request = JSON.stringify({
|
||||
request: "GetActions",
|
||||
id: "GetActions"
|
||||
});
|
||||
|
||||
ws.send(request);
|
||||
}
|
||||
|
||||
// Check if all the entries in targetActionNames exist in actionList
|
||||
function sbCheckRequiredActions(actionList, targetActionNames) {
|
||||
let foundActions = 0;
|
||||
for (targetActionName of targetActionNames) {
|
||||
if (actionList.some(action => action.name === targetActionName))
|
||||
foundActions++;
|
||||
}
|
||||
|
||||
return foundActions == targetActionNames.length
|
||||
}
|
||||
|
||||
function sbFetchBroadcasts(ws) {
|
||||
|
||||
let request = JSON.stringify({
|
||||
request: "DoAction",
|
||||
id: generateUUID(),
|
||||
action: {
|
||||
name: "[NUT] Multistream Title Updater | Fetch Broadcasts"
|
||||
}
|
||||
});
|
||||
|
||||
ws.send(request);
|
||||
}
|
||||
|
||||
function sbUpdateTitles(ws, title) {
|
||||
let request = JSON.stringify({
|
||||
request: "DoAction",
|
||||
id: generateUUID(),
|
||||
action: {
|
||||
name: "[NUT] Multistream Title Updater | Update All Broadcasts"
|
||||
},
|
||||
args: {
|
||||
title: title
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
ws.send(request);
|
||||
}
|
||||
|
||||
function sbUpdateTwitchTitle(ws, title) {
|
||||
let request = JSON.stringify({
|
||||
request: "DoAction",
|
||||
id: generateUUID(),
|
||||
action: {
|
||||
name: "[NUT] Multistream Title Updater | Update Twitch Title"
|
||||
},
|
||||
args: {
|
||||
title: title
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
ws.send(request);
|
||||
}
|
||||
|
||||
function sbUpdateYouTubeTitle(ws, title, broadcastId) {
|
||||
let request = JSON.stringify({
|
||||
request: "DoAction",
|
||||
id: generateUUID(),
|
||||
action: {
|
||||
name: "[NUT] Multistream Title Updater | Update YouTube Title"
|
||||
},
|
||||
args: {
|
||||
broadcastId: broadcastId,
|
||||
title: title
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
ws.send(request);
|
||||
}
|
||||
|
||||
|
||||
//////////////////////
|
||||
// HELPER FUNCTIONS //
|
||||
//////////////////////
|
||||
|
||||
function generateUUID() { // Public Domain/MIT
|
||||
var d = new Date().getTime();//Timestamp
|
||||
var d2 = ((typeof performance !== 'undefined') && performance.now && (performance.now() * 1000)) || 0;//Time in microseconds since page-load or 0 if unsupported
|
||||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
|
||||
var r = Math.random() * 16;//random number between 0 and 16
|
||||
if (d > 0) {//Use timestamp until depleted
|
||||
r = (d + r) % 16 | 0;
|
||||
d = Math.floor(d / 16);
|
||||
} else {//Use microseconds since page-load if supported
|
||||
r = (d2 + r) % 16 | 0;
|
||||
d2 = Math.floor(d2 / 16);
|
||||
}
|
||||
return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16);
|
||||
});
|
||||
}
|
||||
|
||||
function IsNullOrWhitespace(str) {
|
||||
return /^\s*$/.test(str);
|
||||
}
|
||||
|
||||
function SetElementVisibility(elementID, visibility) {
|
||||
let element = document.getElementById(elementID);
|
||||
if (visibility)
|
||||
element.style.display = 'inline';
|
||||
else
|
||||
element.style.display = 'none';
|
||||
}
|
||||
|
||||
function ReadLinesFromFile(filePath) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = new XMLHttpRequest();
|
||||
request.open('GET', filePath, true);
|
||||
request.onload = function () {
|
||||
if (request.status === 200) {
|
||||
resolve(request.responseText.split(/\r?\n/));
|
||||
} else {
|
||||
reject(new Error(`File loading failed with status: ${request.status}`));
|
||||
}
|
||||
};
|
||||
request.onerror = function () {
|
||||
reject(new Error('Network error occurred during file loading.'));
|
||||
};
|
||||
request.send();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////////////
|
||||
// STREAMER.BOT WEBSOCKET STATUS //
|
||||
///////////////////////////////////
|
||||
|
||||
// This function sets the visibility of the Streamer.bot status label on the overlay
|
||||
function SetConnectionStatus(connected) {
|
||||
let connectionStatusIcon = document.getElementById("connectionStatusIcon");
|
||||
let ThisIsWhereAllTheCoolStuffHappens = document.getElementById("ThisIsWhereAllTheCoolStuffHappens");
|
||||
|
||||
if (connected) {
|
||||
connectionStatusIcon.src = "connected.svg";
|
||||
ThisIsWhereAllTheCoolStuffHappens.classList.remove('disabled');
|
||||
SetElementVisibility("infoIcon", false);
|
||||
SetElementVisibility("streamerbotConnectInstructions", false);
|
||||
}
|
||||
else {
|
||||
connectionStatusIcon.src = "disconnected.svg";
|
||||
ThisIsWhereAllTheCoolStuffHappens.classList.add('disabled');
|
||||
SetElementVisibility("infoIcon", true);
|
||||
SetElementVisibility("streamerbotConnectInstructions", true);
|
||||
|
||||
// (1) Clear list of broadcasts
|
||||
const fieldsContainer = document.getElementById('fieldsContainer');
|
||||
fieldsContainer.innerHTML = "";
|
||||
}
|
||||
}
|
||||
|
||||
// This function sets the visibility of the Streamer.bot status label on the overlay
|
||||
function RefreshAnimation() {
|
||||
let refreshContainer = document.getElementById("refreshContainer");
|
||||
refreshContainer.style.opacity = 1;
|
||||
var tl = new TimelineMax();
|
||||
tl
|
||||
.to(refreshContainer, 2, { opacity: 0, ease: Linear.easeNone })
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Button handling for UPDATE ALL button only
|
||||
const infoIcon = document.querySelector("#infoIcon");
|
||||
infoIcon.addEventListener("click", function () {
|
||||
window.open("https://www.notion.so/nutty-s-multistream-title-updater-e2fb7b24c7514f9cbd943729f54be1d9");
|
||||
});
|
||||
|
||||
// Button handling for UPDATE ALL button only
|
||||
const submitButton = document.querySelector("#submitButton");
|
||||
submitButton.addEventListener("click", function () {
|
||||
const titleInput = document.querySelector("#titleInput").value;
|
||||
sbUpdateTitles(ws, titleInput);
|
||||
});
|
||||
|
||||
// Button handling for REFRESH button only
|
||||
const refreshButton = document.querySelector("#refreshButton");
|
||||
refreshButton.addEventListener("click", function () {
|
||||
sbFetchBroadcasts(ws);
|
||||
});
|
||||
|
||||
|
||||
function UpdateBroadcastList(data) {
|
||||
// Iterate through broadcast list
|
||||
// Find div whose ID matches the broadcast
|
||||
// If it exists, update the title
|
||||
// else, it's a new broadcast, so add it to the list
|
||||
const fieldsContainer = document.getElementById('fieldsContainer');
|
||||
for (const broadcast of data.broadcastList) {
|
||||
const broadcastDiv = fieldsContainer.querySelector(`#${broadcast.id}`);
|
||||
|
||||
if (broadcastDiv == null)
|
||||
AddBroadcast(broadcast);
|
||||
else
|
||||
UpdateBroadcast(broadcastDiv, broadcast);
|
||||
}
|
||||
|
||||
// Check if any broadcasts have gone offline
|
||||
// If so, delete them from the list
|
||||
var parentDiv = document.getElementById('fieldsContainer');
|
||||
const childDivs = parentDiv.querySelectorAll("div");
|
||||
childDivs.forEach(childDiv => {
|
||||
var result = data.broadcastList.find(obj => {
|
||||
return obj.id === childDiv.id
|
||||
})
|
||||
if (result == null)
|
||||
childDiv.innerHTML = "";
|
||||
});
|
||||
|
||||
//// THIS CODE ALSO WORKS AND IS WAY SIMPLER, I JUST MADE THINGS
|
||||
//// 10 TIMES HARDER FOR MYSELF BECAUSE I LOVE PAIN POGGERS
|
||||
// // (1) Clear list of broadcasts
|
||||
// const fieldsContainer = document.getElementById('fieldsContainer');
|
||||
// fieldsContainer.innerHTML = "";
|
||||
|
||||
// // (2) For each broadcast in list, create new entry
|
||||
// for (const broadcast of data.broadcastList)
|
||||
// {
|
||||
// AddBroadcast(broadcast);
|
||||
// }
|
||||
|
||||
// Count the number of YouTube broadcasts
|
||||
// If this is 0, put a message to tell the user that they need to go live on YouTube first
|
||||
// Else, hide that message
|
||||
const youtubeBroadcastCount = data.broadcastList.reduce((count, broadcast) => count + (broadcast.platform === "youtube" ? 1 : 0), 0);
|
||||
if (youtubeBroadcastCount <= 0)
|
||||
SetElementVisibility("noYouTubeStreamsInstructions", true);
|
||||
else
|
||||
SetElementVisibility("noYouTubeStreamsInstructions", false);
|
||||
|
||||
RefreshAnimation();
|
||||
}
|
||||
|
||||
function AddBroadcast(broadcast) {
|
||||
// Get a reference to the template
|
||||
const template = document.getElementById('platformTemplate');
|
||||
|
||||
// Create a new instance of the template
|
||||
const instance = template.content.cloneNode(true);
|
||||
|
||||
// Assign ID to instance
|
||||
const broadcastBox = instance.querySelector('.broadcastBox');
|
||||
broadcastBox.id = broadcast.id;
|
||||
|
||||
// Modify the content of the template instance
|
||||
const titleElement = instance.querySelector('.platformTitle');
|
||||
titleElement.value = broadcast.title;
|
||||
|
||||
// Modify the content of the template instance
|
||||
const buttonElement = instance.querySelector('.platformSubmitButton');
|
||||
buttonElement.classList.add(broadcast.platform);
|
||||
|
||||
// Modify the icon of the template instance
|
||||
const iconElement = instance.querySelector('#icon');
|
||||
|
||||
// Add button handling (need separate handling for Twitch/YouTube)
|
||||
switch (broadcast.platform) {
|
||||
case 'twitch':
|
||||
iconElement.src = 'twitch.png';
|
||||
|
||||
buttonElement.addEventListener("click", function () {
|
||||
sbUpdateTwitchTitle(ws, titleElement.value);
|
||||
});
|
||||
break;
|
||||
case 'youtube':
|
||||
// Modify the icon of the template instance
|
||||
iconElement.src = 'youtube.png';
|
||||
|
||||
buttonElement.addEventListener("click", function () {
|
||||
const youtubeID = broadcast.id.replace('youtube-', '');
|
||||
sbUpdateYouTubeTitle(ws, titleElement.value, youtubeID);
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
// Add click event
|
||||
const iconButton = instance.querySelector('#platformIconButton');
|
||||
iconButton.addEventListener("click", function () {
|
||||
window.open(broadcast.url);
|
||||
});
|
||||
|
||||
// Insert the modified template instance into the DOM
|
||||
const fieldsContainer = document.getElementById('fieldsContainer');
|
||||
fieldsContainer.appendChild(instance);
|
||||
}
|
||||
|
||||
function UpdateBroadcast(div, broadcast) {
|
||||
// Modify the content of the template instance
|
||||
const titleElement = div.querySelector('.platformTitle');
|
||||
titleElement.value = broadcast.title;
|
||||
}
|
||||
|
||||
connectws();
|
||||
@@ -0,0 +1,201 @@
|
||||
/* @import url('https://fonts.googleapis.com/css?family={{googleFont}}:100,200,300,400,500,600,700,800,900'); */
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica, Arial, sans-serif;
|
||||
color: rgb(235, 235, 235);
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #1f1f1f;
|
||||
}
|
||||
|
||||
#refreshContainer {
|
||||
font-family: 'Metropolis';
|
||||
font-style: italic;
|
||||
font-weight: bold;
|
||||
font-size: 30px;
|
||||
width: 400px;
|
||||
max-width: fit-content;
|
||||
text-align: center;
|
||||
background-color: #2FB774;
|
||||
opacity: 0;
|
||||
color: white;
|
||||
padding: 10px;
|
||||
border-radius: 10px;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
#instructionsContainer {
|
||||
background-color: rgb(59, 59, 59);
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
border-radius: 10px;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
text-align: center;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.hyperlinks {
|
||||
color: rgb(79, 92, 212);
|
||||
}
|
||||
|
||||
#mainContainer
|
||||
{
|
||||
text-transform: uppercase;
|
||||
max-width: 500px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
padding: 20px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.disabled
|
||||
{
|
||||
pointer-events: none;
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
#title
|
||||
{
|
||||
flex-grow: 1;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
#connectionStatusIcon
|
||||
{
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
#infoIcon
|
||||
{
|
||||
background-color: transparent;
|
||||
background-repeat: no-repeat;
|
||||
border: none;
|
||||
overflow: hidden;
|
||||
outline: none;
|
||||
opacity: 0.5;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
#infoIcon:hover
|
||||
{
|
||||
opacity: 1;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.textBox
|
||||
{
|
||||
padding: 5px 15px;
|
||||
border-radius:20px;
|
||||
background:rgba(255,255,255,.1);
|
||||
border-width: 0;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.straightUpIloveCum
|
||||
{
|
||||
display: flex;
|
||||
margin: 0px 0px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.button
|
||||
{
|
||||
border: none;
|
||||
padding: 5px 15px;
|
||||
border-radius:25px;
|
||||
}
|
||||
|
||||
.button:hover
|
||||
{
|
||||
box-shadow: 0 0px 10px 0 rgb(255, 255, 255, 0.3);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.submitButton
|
||||
{
|
||||
background:rgba(17, 97, 238, 0.2);
|
||||
}
|
||||
|
||||
.submitButton:hover{
|
||||
background-color: rgba(17, 97, 238, 1);
|
||||
}
|
||||
|
||||
#refreshButton
|
||||
{
|
||||
background:rgba(255,255,255,.1);
|
||||
margin: 0px 0px 0px 10px;
|
||||
}
|
||||
|
||||
#refreshButton:hover{
|
||||
background:rgba(255,255,255,.5);
|
||||
}
|
||||
|
||||
#fieldsContainer
|
||||
{
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.broadcastBox
|
||||
{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 10px 0px;
|
||||
}
|
||||
|
||||
.platformTitle
|
||||
{
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.platformSubmitButton
|
||||
{
|
||||
width: 100px;
|
||||
margin: 0px 0px 0px 10px;
|
||||
}
|
||||
|
||||
#platformIconButton
|
||||
{
|
||||
background-color: transparent;
|
||||
background-repeat: no-repeat;
|
||||
border: none;
|
||||
overflow: hidden;
|
||||
outline: none;
|
||||
opacity: 0.5;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
#platformIconButton:hover
|
||||
{
|
||||
opacity: 1;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.twitch
|
||||
{
|
||||
background: rgb(110, 85, 171, 0.5);
|
||||
}
|
||||
|
||||
.twitch:hover
|
||||
{
|
||||
background: rgb(110, 85, 171, 1);
|
||||
}
|
||||
|
||||
.youtube
|
||||
{
|
||||
background: rgb(255, 3, 3, 0.5);
|
||||
}
|
||||
|
||||
.youtube:hover
|
||||
{
|
||||
background: rgb(255, 3, 3, 1);
|
||||
}
|
||||
|
After Width: | Height: | Size: 55 KiB |
|
After Width: | Height: | Size: 65 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 254 KiB |
|
After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 604 KiB |
|
After Width: | Height: | Size: 203 KiB |
|
After Width: | Height: | Size: 20 KiB |
@@ -0,0 +1,80 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="shortcut icon" href="#" />
|
||||
<link rel="stylesheet" href="./style.css" />
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@xz/fonts@1/serve/metropolis.min.css" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="statusContainer">Connecting...</div>
|
||||
<div id="mainContainer">
|
||||
<div id="infoContainer">
|
||||
<div id="streamInfo">
|
||||
<label id="streamPlatformLabel"></label>
|
||||
<label>•</label>
|
||||
<label id="streamTimecodeLabel"></label>
|
||||
<label>•</label>
|
||||
<label id="streamBitrateLabel"></label>
|
||||
</div>
|
||||
|
||||
<div id="topBar">
|
||||
<div id="profileLabel"></div>
|
||||
</div>
|
||||
|
||||
<div id="recordInfo">
|
||||
<label id="recordOutputFilesize"></label>
|
||||
<label>•</label>
|
||||
<label id="recordTimecodeLabel"></label>
|
||||
<label>•</label>
|
||||
<label id="recordingLabel"></label>
|
||||
</div>
|
||||
|
||||
<div id="bottomBar">
|
||||
<div id="statsLabel"></div>
|
||||
<div id="advancedStatsLabel"></div>
|
||||
</div>
|
||||
|
||||
<div id="theMotherOfAllVolumeContainers">
|
||||
<div id="volumeMeterContainerLeft">
|
||||
<div id="theGreenShitThatsInsideTheOtherContainerLeft">
|
||||
</div>
|
||||
<div class="barLabel">L</div>
|
||||
</div>
|
||||
|
||||
<div id="volumeMeterContainerRight">
|
||||
<div id="theGreenShitThatsInsideTheOtherContainerRight">
|
||||
</div>
|
||||
<div class="barLabel">R</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="fpsContainer">
|
||||
<div id="fpsMeter"></div>
|
||||
<div id="fpsLabel">
|
||||
FPS
|
||||
<label id="fps"></label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<img id="micMuteIcon" src="icons/mic-mute.png">
|
||||
</div>
|
||||
<div id="streamingRing">
|
||||
<div id="streamingLeft"></div>
|
||||
<div id="streamingRight"></div>
|
||||
<div id="streamingTop"></div>
|
||||
<div id="streamingBottom"></div>
|
||||
</div>
|
||||
<div id="recordingRing">
|
||||
<div id="left"></div>
|
||||
<div id="right"></div>
|
||||
<div id="top"></div>
|
||||
<div id="bottom"></div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
<script src='https://cdnjs.cloudflare.com/ajax/libs/gsap/1.18.0/TweenMax.min.js'></script>
|
||||
<script src="./script.js"></script>
|
||||
@@ -0,0 +1,492 @@
|
||||
////////////////
|
||||
// PARAMETERS //
|
||||
////////////////
|
||||
|
||||
const queryString = window.location.search;
|
||||
const urlParams = new URLSearchParams(queryString);
|
||||
|
||||
const obsServerAddress = urlParams.get("address") || "127.0.0.1";
|
||||
const obsServerPort = urlParams.get("port") || "4455";
|
||||
const obsServerPassword = urlParams.get("password") || "";
|
||||
const obsMicInput = urlParams.get("audio") || "";
|
||||
const background = urlParams.get("background") || "";
|
||||
|
||||
if (obsMicInput != "")
|
||||
document.getElementById("theMotherOfAllVolumeContainers").style.visibility = "visible";
|
||||
|
||||
if (background != "")
|
||||
{
|
||||
document.body.style.backgroundImage = `url("frames/${background}.png")`
|
||||
}
|
||||
|
||||
let ws = new WebSocket("ws://" + obsServerAddress + ":" + obsServerPort + "/");
|
||||
|
||||
let previousOutputTimecode = 0;
|
||||
let previousOutputBytes = 0;
|
||||
let activeFps;
|
||||
|
||||
function connectws() {
|
||||
if ("WebSocket" in window) {
|
||||
|
||||
ws = new WebSocket("ws://" + obsServerAddress + ":" + obsServerPort + "/");
|
||||
|
||||
// Reconnect
|
||||
ws.onclose = function () {
|
||||
SetConnectionStatus(false);
|
||||
setTimeout(connectws, 5000);
|
||||
};
|
||||
|
||||
ws.onopen = async function () {
|
||||
}
|
||||
|
||||
ws.onmessage = async function (event) {
|
||||
let data = JSON.parse(event.data);
|
||||
|
||||
switch (data.op) {
|
||||
case 0: // Hello OpCode
|
||||
case 3: // Reidentify OpCode
|
||||
let salt = data.d.authentication != null ? data.d.authentication.salt : "";
|
||||
let challenge = data.d.authentication != null ? data.d.authentication.challenge : "";
|
||||
|
||||
let secret = await sha256(obsServerPassword + salt);
|
||||
let base64_secret = hexToBase64(secret);
|
||||
|
||||
let auth_string = await sha256(base64_secret + challenge);
|
||||
let base64_auth_string = hexToBase64(auth_string);
|
||||
|
||||
ws.send(
|
||||
JSON.stringify({
|
||||
op: 1,
|
||||
d: {
|
||||
rpcVersion: 1,
|
||||
authentication: base64_auth_string,
|
||||
eventSubscriptions: 1 << 16
|
||||
}
|
||||
}
|
||||
));
|
||||
break;
|
||||
case 2: // Identify OpCode
|
||||
console.log("Connected to OBS!");
|
||||
SetConnectionStatus(true);
|
||||
break;
|
||||
case 5: // Event OpCode
|
||||
switch (data.d.eventType) {
|
||||
case ("InputVolumeMeters"):
|
||||
let eventData = data.d.eventData;
|
||||
eventData.inputs.forEach((input) => {
|
||||
if (input.inputName == obsMicInput) {
|
||||
if (input.inputLevelsMul.length == 0) {
|
||||
document.getElementById("theMotherOfAllVolumeContainers").style.visibility = `hidden`;
|
||||
}
|
||||
else {
|
||||
let leftMeter = document.getElementById("theGreenShitThatsInsideTheOtherContainerLeft");
|
||||
let rightMeter = document.getElementById("theGreenShitThatsInsideTheOtherContainerRight");
|
||||
|
||||
var tl = new TimelineMax();
|
||||
tl
|
||||
.to(leftMeter, 0.1, { height: + 100 * input.inputLevelsMul[0][1] + "%", ease: Linear.easeNone });
|
||||
|
||||
tl = new TimelineMax();
|
||||
tl
|
||||
.to(rightMeter, 0.1, { height: + 100 * input.inputLevelsMul[1][1] + "%", ease: Linear.easeNone });
|
||||
|
||||
document.getElementById("theMotherOfAllVolumeContainers").style.visibility = `visible`;
|
||||
}
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case 7: // RequestResponse OpCode
|
||||
switch (data.d.requestType) {
|
||||
case "GetStats":
|
||||
{
|
||||
let responseData = data.d.responseData;
|
||||
activeFps = `${responseData.activeFps.toFixed(1)}`;
|
||||
const cpu = `${responseData.cpuUsage.toFixed(1)}%`;
|
||||
const memory = `${responseData.memoryUsage.toFixed(1)}MB`;
|
||||
|
||||
const averageFrameRenderTime = `${responseData.averageFrameRenderTime.toFixed(1)}ms`;
|
||||
const outputSkippedFrames = responseData.outputSkippedFrames;
|
||||
const outputTotalFrames = responseData.outputTotalFrames;
|
||||
const outputSkippedFramesPerc = outputTotalFrames > 0 ? `${(100 * outputSkippedFrames / outputTotalFrames).toFixed(1)}%` : `0%`;
|
||||
const renderSkippedFrames = responseData.renderSkippedFrames;
|
||||
const renderTotalFrames = responseData.renderTotalFrames;
|
||||
const renderSkippedFramesPerc = `${(100 * renderSkippedFrames / renderTotalFrames).toFixed(1)}%`
|
||||
|
||||
document.getElementById("statsLabel").innerHTML = `CPU: ${cpu} • MEM: ${memory} • RENDER TIME: ${averageFrameRenderTime}`;
|
||||
document.getElementById("advancedStatsLabel").innerHTML = `MISSED FRAMES ${outputSkippedFramesPerc} • SKIPPED FRAMES ${renderSkippedFramesPerc}`;
|
||||
document.getElementById("fps").innerHTML = `${activeFps}`;
|
||||
|
||||
GetVideoSettings();
|
||||
}
|
||||
break;
|
||||
case "GetVideoSettings":
|
||||
{
|
||||
let responseData = data.d.responseData;
|
||||
const fpsNumerator = responseData.fpsNumerator;
|
||||
const fpsDenominator = responseData.fpsDenominator;
|
||||
|
||||
const fps = fpsNumerator / fpsDenominator;
|
||||
const fpsMeterValue = activeFps / fps;
|
||||
|
||||
let fpsMeter = document.getElementById("fpsMeter");
|
||||
var tl = new TimelineMax();
|
||||
tl
|
||||
.to(fpsMeter, 0.1, { height: + 100 * fpsMeterValue + "%", ease: Linear.easeNone });
|
||||
|
||||
if (fpsMeterValue >= 1)
|
||||
document.getElementById("fpsMeter").style.backgroundColor = `#37d247`;
|
||||
else if (fpsMeterValue > 0.9)
|
||||
document.getElementById("fpsMeter").style.backgroundColor = `#e5af24`;
|
||||
else
|
||||
document.getElementById("fpsMeter").style.backgroundColor = `#D12025`;
|
||||
}
|
||||
break;
|
||||
case "GetRecordStatus":
|
||||
{
|
||||
let responseData = data.d.responseData;
|
||||
|
||||
if (responseData.outputActive === false) {
|
||||
document.getElementById("recordingLabel").innerHTML = ``;
|
||||
document.getElementById("recordTimecodeLabel").innerHTML = ``;
|
||||
document.getElementById("recordOutputFilesize").innerHTML = ``;
|
||||
document.getElementById("recordingRing").style.visibility = 'hidden';
|
||||
document.getElementById("recordInfo").style.visibility = `hidden`;
|
||||
}
|
||||
else {
|
||||
document.getElementById("recordingLabel").innerHTML = `REC 🔴`;
|
||||
document.getElementById("recordTimecodeLabel").innerHTML = `${RemoveMilliseconds(responseData.outputTimecode)}`;
|
||||
document.getElementById("recordOutputFilesize").innerHTML = `${ConvertToMegabytes(responseData.outputBytes)}MB`;
|
||||
document.getElementById("recordingRing").style.visibility = 'visible';
|
||||
document.getElementById("recordInfo").style.visibility = `visible`;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case "GetStreamStatus":
|
||||
{
|
||||
let responseData = data.d.responseData;
|
||||
|
||||
if (responseData.outputActive === false) {
|
||||
document.getElementById("streamingRing").style.visibility = 'hidden';
|
||||
document.getElementById("streamInfo").style.visibility = `hidden`;
|
||||
}
|
||||
else {
|
||||
let outputTimecode = TimeToMilliseconds(responseData.outputTimecode);
|
||||
let outputBytes = responseData.outputBytes;
|
||||
|
||||
let kbps = ((outputBytes - previousOutputBytes) / (outputTimecode - previousOutputTimecode) * 8);
|
||||
|
||||
previousOutputTimecode = outputTimecode;
|
||||
previousOutputBytes = outputBytes;
|
||||
|
||||
document.getElementById("streamBitrateLabel").innerHTML = `${Math.floor(kbps)} kb/s`;
|
||||
document.getElementById("streamTimecodeLabel").innerHTML = `${RemoveMilliseconds(responseData.outputTimecode)}`;
|
||||
GetStreamServiceSettings();
|
||||
|
||||
document.getElementById("streamingRing").style.visibility = 'visible';
|
||||
document.getElementById("streamInfo").style.visibility = `visible`;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case "GetStreamServiceSettings":
|
||||
{
|
||||
let responseData = data.d.responseData;
|
||||
switch (responseData.streamServiceSettings.service) {
|
||||
case "Twitch":
|
||||
document.getElementById("streamPlatformLabel").innerHTML = "🟣 Twitch";
|
||||
break;
|
||||
case "YouTube":
|
||||
document.getElementById("streamPlatformLabel").innerHTML = "🔴 YouTube";
|
||||
break;
|
||||
case undefined:
|
||||
document.getElementById("streamPlatformLabel").innerHTML = "🔴 LIVE";
|
||||
break;
|
||||
default:
|
||||
document.getElementById("streamPlatformLabel").innerHTML = `🔴 ${responseData.streamServiceSettings.service}`;
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case "GetProfileList":
|
||||
{
|
||||
let responseData = data.d.responseData;
|
||||
document.getElementById("profileLabel").innerHTML = `Profile: ${responseData.currentProfileName}`;
|
||||
}
|
||||
break;
|
||||
case "GetInputMute":
|
||||
{
|
||||
let responseData = data.d.responseData;
|
||||
console.log(responseData);
|
||||
if (responseData.inputMuted)
|
||||
document.getElementById("micMuteIcon").style.visibility = `visible`;
|
||||
else
|
||||
document.getElementById("micMuteIcon").style.visibility = `hidden`;
|
||||
}
|
||||
break;
|
||||
}
|
||||
break;
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
//////////////////////
|
||||
// HELPER FUNCTIONS //
|
||||
//////////////////////
|
||||
|
||||
function obswsSendRequest(ws, data) {
|
||||
ws.send(
|
||||
JSON.stringify({
|
||||
"op": 6,
|
||||
"d": data
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
function TimeToMilliseconds(hms) {
|
||||
const [hours, minutes, seconds] = hms.split(':');
|
||||
const totalSeconds = (+hours) * 60 * 60 + (+minutes) * 60 + (+seconds);
|
||||
return totalSeconds * 1000;
|
||||
}
|
||||
|
||||
function RemoveMilliseconds(timecode) {
|
||||
const parts = timecode.split('.');
|
||||
return parts[0];
|
||||
}
|
||||
|
||||
function ConvertToMegabytes(bytes) {
|
||||
return ((bytes / 1024) / 1024).toFixed(2);
|
||||
}
|
||||
|
||||
function CreateGuid() {
|
||||
function _p8(s) {
|
||||
var p = (Math.random().toString(16) + "000000000").substr(2, 8);
|
||||
return s ? "-" + p.substr(0, 4) + "-" + p.substr(4, 4) : p;
|
||||
}
|
||||
return _p8() + _p8(true) + _p8(true) + _p8();
|
||||
}
|
||||
|
||||
function sha256(ascii) {
|
||||
function rightRotate(value, amount) {
|
||||
return (value >>> amount) | (value << (32 - amount));
|
||||
};
|
||||
|
||||
var mathPow = Math.pow;
|
||||
var maxWord = mathPow(2, 32);
|
||||
var lengthProperty = 'length'
|
||||
var i, j; // Used as a counter across the whole file
|
||||
var result = ''
|
||||
|
||||
var words = [];
|
||||
var asciiBitLength = ascii[lengthProperty] * 8;
|
||||
|
||||
//* caching results is optional - remove/add slash from front of this line to toggle
|
||||
// Initial hash value: first 32 bits of the fractional parts of the square roots of the first 8 primes
|
||||
// (we actually calculate the first 64, but extra values are just ignored)
|
||||
var hash = sha256.h = sha256.h || [];
|
||||
// Round constants: first 32 bits of the fractional parts of the cube roots of the first 64 primes
|
||||
var k = sha256.k = sha256.k || [];
|
||||
var primeCounter = k[lengthProperty];
|
||||
/*/
|
||||
var hash = [], k = [];
|
||||
var primeCounter = 0;
|
||||
//*/
|
||||
|
||||
var isComposite = {};
|
||||
for (var candidate = 2; primeCounter < 64; candidate++) {
|
||||
if (!isComposite[candidate]) {
|
||||
for (i = 0; i < 313; i += candidate) {
|
||||
isComposite[i] = candidate;
|
||||
}
|
||||
hash[primeCounter] = (mathPow(candidate, .5) * maxWord) | 0;
|
||||
k[primeCounter++] = (mathPow(candidate, 1 / 3) * maxWord) | 0;
|
||||
}
|
||||
}
|
||||
|
||||
ascii += '\x80' // Append Ƈ' bit (plus zero padding)
|
||||
while (ascii[lengthProperty] % 64 - 56) ascii += '\x00' // More zero padding
|
||||
for (i = 0; i < ascii[lengthProperty]; i++) {
|
||||
j = ascii.charCodeAt(i);
|
||||
if (j >> 8) return; // ASCII check: only accept characters in range 0-255
|
||||
words[i >> 2] |= j << ((3 - i) % 4) * 8;
|
||||
}
|
||||
words[words[lengthProperty]] = ((asciiBitLength / maxWord) | 0);
|
||||
words[words[lengthProperty]] = (asciiBitLength)
|
||||
|
||||
// process each chunk
|
||||
for (j = 0; j < words[lengthProperty];) {
|
||||
var w = words.slice(j, j += 16); // The message is expanded into 64 words as part of the iteration
|
||||
var oldHash = hash;
|
||||
// This is now the undefinedworking hash", often labelled as variables a...g
|
||||
// (we have to truncate as well, otherwise extra entries at the end accumulate
|
||||
hash = hash.slice(0, 8);
|
||||
|
||||
for (i = 0; i < 64; i++) {
|
||||
var i2 = i + j;
|
||||
// Expand the message into 64 words
|
||||
// Used below if
|
||||
var w15 = w[i - 15], w2 = w[i - 2];
|
||||
|
||||
// Iterate
|
||||
var a = hash[0], e = hash[4];
|
||||
var temp1 = hash[7]
|
||||
+ (rightRotate(e, 6) ^ rightRotate(e, 11) ^ rightRotate(e, 25)) // S1
|
||||
+ ((e & hash[5]) ^ ((~e) & hash[6])) // ch
|
||||
+ k[i]
|
||||
// Expand the message schedule if needed
|
||||
+ (w[i] = (i < 16) ? w[i] : (
|
||||
w[i - 16]
|
||||
+ (rightRotate(w15, 7) ^ rightRotate(w15, 18) ^ (w15 >>> 3)) // s0
|
||||
+ w[i - 7]
|
||||
+ (rightRotate(w2, 17) ^ rightRotate(w2, 19) ^ (w2 >>> 10)) // s1
|
||||
) | 0
|
||||
);
|
||||
// This is only used once, so *could* be moved below, but it only saves 4 bytes and makes things unreadble
|
||||
var temp2 = (rightRotate(a, 2) ^ rightRotate(a, 13) ^ rightRotate(a, 22)) // S0
|
||||
+ ((a & hash[1]) ^ (a & hash[2]) ^ (hash[1] & hash[2])); // maj
|
||||
|
||||
hash = [(temp1 + temp2) | 0].concat(hash); // We don't bother trimming off the extra ones, they're harmless as long as we're truncating when we do the slice()
|
||||
hash[4] = (hash[4] + temp1) | 0;
|
||||
}
|
||||
|
||||
for (i = 0; i < 8; i++) {
|
||||
hash[i] = (hash[i] + oldHash[i]) | 0;
|
||||
}
|
||||
}
|
||||
|
||||
for (i = 0; i < 8; i++) {
|
||||
for (j = 3; j + 1; j--) {
|
||||
var b = (hash[i] >> (j * 8)) & 255;
|
||||
result += ((b < 16) ? 0 : '') + b.toString(16);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
function hexToBase64(hexstring) {
|
||||
return btoa(hexstring.match(/\w{2}/g).map(function (a) {
|
||||
return String.fromCharCode(parseInt(a, 16));
|
||||
}).join(""));
|
||||
}
|
||||
|
||||
|
||||
|
||||
//////////////////////
|
||||
// WEBSOCKET STATUS //
|
||||
//////////////////////
|
||||
|
||||
// This function sets the visibility of the Streamer.bot status label on the overlay
|
||||
function SetConnectionStatus(connected) {
|
||||
let statusContainer = document.getElementById("statusContainer");
|
||||
|
||||
if (connected) {
|
||||
statusContainer.style.background = "#2FB774";
|
||||
statusContainer.innerText = "Connected!";
|
||||
mainContainer.style.visibility = `visible`;
|
||||
var tl = new TimelineMax();
|
||||
tl
|
||||
.to(statusContainer, 2, { opacity: 0, ease: Linear.easeNone });
|
||||
}
|
||||
else {
|
||||
statusContainer.style.background = "#D12025";
|
||||
statusContainer.innerText = "Connecting...";
|
||||
statusContainer.style.opacity = 1;
|
||||
mainContainer.style.visibility = `hidden`;
|
||||
}
|
||||
}
|
||||
|
||||
connectws();
|
||||
|
||||
setInterval(GetStreamStatus, 1000);
|
||||
function GetStreamStatus() {
|
||||
if (ws.readyState !== WebSocket.CLOSED) {
|
||||
let data =
|
||||
{
|
||||
"requestType": "GetStreamStatus",
|
||||
"requestId": CreateGuid(),
|
||||
"requestData": {
|
||||
}
|
||||
}
|
||||
obswsSendRequest(ws, data);
|
||||
}
|
||||
}
|
||||
|
||||
function GetStreamServiceSettings() {
|
||||
let data =
|
||||
{
|
||||
"requestType": "GetStreamServiceSettings",
|
||||
"requestId": CreateGuid(),
|
||||
"requestData": {
|
||||
}
|
||||
}
|
||||
obswsSendRequest(ws, data);
|
||||
}
|
||||
|
||||
setInterval(GetProfileList, 200);
|
||||
function GetProfileList() {
|
||||
let data =
|
||||
{
|
||||
"requestType": "GetProfileList",
|
||||
"requestId": CreateGuid(),
|
||||
"requestData": {
|
||||
}
|
||||
}
|
||||
obswsSendRequest(ws, data);
|
||||
}
|
||||
|
||||
setInterval(GetStats, 1000);
|
||||
function GetStats() {
|
||||
let data =
|
||||
{
|
||||
"requestType": "GetStats",
|
||||
"requestId": CreateGuid(),
|
||||
"requestData": {
|
||||
}
|
||||
}
|
||||
obswsSendRequest(ws, data);
|
||||
}
|
||||
|
||||
setInterval(GetRecordStatus, 500);
|
||||
function GetRecordStatus() {
|
||||
let data =
|
||||
{
|
||||
"requestType": "GetRecordStatus",
|
||||
"requestId": CreateGuid(),
|
||||
"requestData": {
|
||||
}
|
||||
}
|
||||
obswsSendRequest(ws, data);
|
||||
}
|
||||
|
||||
|
||||
if (obsMicInput != "")
|
||||
{
|
||||
setInterval(GetInputMute, 500);
|
||||
function GetInputMute() {
|
||||
let data =
|
||||
{
|
||||
"requestType": "GetInputMute",
|
||||
"requestId": CreateGuid(),
|
||||
"requestData": {
|
||||
"inputName": obsMicInput
|
||||
}
|
||||
}
|
||||
obswsSendRequest(ws, data);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function GetVideoSettings() {
|
||||
let data =
|
||||
{
|
||||
"requestType": "GetVideoSettings",
|
||||
"requestId": CreateGuid(),
|
||||
"requestData": {
|
||||
}
|
||||
}
|
||||
obswsSendRequest(ws, data);
|
||||
}
|
||||
@@ -0,0 +1,257 @@
|
||||
* {
|
||||
--background: RGB(0, 0, 0, 0.75);
|
||||
}
|
||||
|
||||
body {
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
#statusContainer {
|
||||
font-family: 'Metropolis';
|
||||
font-style: italic;
|
||||
font-weight: bold;
|
||||
font-size: 30px;
|
||||
width: 400px;
|
||||
max-width: fit-content;
|
||||
text-align: center;
|
||||
background-color: #D12025;
|
||||
color: white;
|
||||
padding: 10px;
|
||||
border-radius: 10px;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
#top,
|
||||
#bottom,
|
||||
#left,
|
||||
#right {
|
||||
background: red;
|
||||
position: fixed;
|
||||
}
|
||||
|
||||
#left,
|
||||
#right {
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 1vw;
|
||||
}
|
||||
|
||||
#left {
|
||||
left: 0;
|
||||
}
|
||||
|
||||
#right {
|
||||
right: 0;
|
||||
}
|
||||
|
||||
#top,
|
||||
#bottom {
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 1vw;
|
||||
}
|
||||
|
||||
#top {
|
||||
top: 0;
|
||||
}
|
||||
|
||||
#bottom {
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
#recordingRing {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
#streamingTop,
|
||||
#streamingBottom,
|
||||
#streamingLeft,
|
||||
#streamingRight {
|
||||
background: #8d65c5;
|
||||
position: fixed;
|
||||
}
|
||||
|
||||
#streamingLeft,
|
||||
#streamingRight {
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 1vw;
|
||||
}
|
||||
|
||||
#streamingLeft {
|
||||
left: 0;
|
||||
}
|
||||
|
||||
#streamingRight {
|
||||
right: 0;
|
||||
}
|
||||
|
||||
#streamingTop,
|
||||
#streamingBottom {
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 1vw;
|
||||
}
|
||||
|
||||
#streamingTop {
|
||||
top: 0;
|
||||
}
|
||||
|
||||
#streamingBottom {
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
|
||||
#streamingRing {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
#infoContainer {
|
||||
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica, Arial, sans-serif;
|
||||
font-weight: 600;
|
||||
font-size: 30px;
|
||||
text-shadow: 2px 2px 2px #000000;
|
||||
color: rgb(235, 235, 235);
|
||||
z-index: 0;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#bottomBar,
|
||||
#topBar,
|
||||
#streamInfo,
|
||||
#recordInfo {
|
||||
position: absolute;
|
||||
background-color: var(--background);
|
||||
white-space: nowrap;
|
||||
padding: 0px 20px;
|
||||
}
|
||||
|
||||
#bottomBar {
|
||||
transform: translate(-50%, 0%);
|
||||
text-align: center;
|
||||
left: 50%;
|
||||
bottom: 4vw;
|
||||
}
|
||||
|
||||
#topBar {
|
||||
transform: translate(-50%, 0%);
|
||||
left: 50%;
|
||||
top: 4vw;
|
||||
}
|
||||
|
||||
#streamInfo {
|
||||
visibility: hidden;
|
||||
left: 2vw;
|
||||
top: 4vw;
|
||||
}
|
||||
|
||||
#recordInfo {
|
||||
visibility: hidden;
|
||||
right: 2vw;
|
||||
top: 4vw;
|
||||
}
|
||||
|
||||
#theMotherOfAllVolumeContainers
|
||||
{
|
||||
position: relative;
|
||||
margin-top: 10vw;
|
||||
margin-bottom: 10vw;
|
||||
height: calc(100% - 20vw);
|
||||
width: 4vw;
|
||||
margin-left: 4vw;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
#volumeMeterContainerLeft, #volumeMeterContainerRight {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 48%;
|
||||
}
|
||||
|
||||
#volumeMeterContainerLeft {
|
||||
background-color: var(--background);
|
||||
position: absolute;
|
||||
bottom: 0%;
|
||||
}
|
||||
|
||||
#volumeMeterContainerRight {
|
||||
background-color: var(--background);
|
||||
right: 0%;
|
||||
bottom: 0%;
|
||||
}
|
||||
|
||||
#theGreenShitThatsInsideTheOtherContainerLeft,
|
||||
#theGreenShitThatsInsideTheOtherContainerRight {
|
||||
background-color: #37d247;
|
||||
position: absolute;
|
||||
bottom: 0%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#theGreenShitThatsInsideTheOtherContainerLeft {
|
||||
height: 00%;
|
||||
}
|
||||
|
||||
#theGreenShitThatsInsideTheOtherContainerRight {
|
||||
height: 00%;
|
||||
}
|
||||
|
||||
#fpsContainer {
|
||||
position: absolute;
|
||||
background-color: var(--background);
|
||||
width: 2vw;
|
||||
height: calc(100% - 20vw);
|
||||
right: 4vw;
|
||||
top: 10vw;
|
||||
}
|
||||
|
||||
#fpsMeter {
|
||||
background-color: #37d247;
|
||||
position: absolute;
|
||||
bottom: 0%;
|
||||
width: 100%;
|
||||
height: 0%;
|
||||
}
|
||||
|
||||
#fpsLabel {
|
||||
font-size: 22px;
|
||||
position: absolute;
|
||||
bottom: -10px;
|
||||
left: 50%;
|
||||
transform: translate(-50%, 100%);
|
||||
text-align: center;
|
||||
background-color: var(--background);
|
||||
}
|
||||
|
||||
#micMuteIcon {
|
||||
visibility: hidden;
|
||||
height: 50%;
|
||||
position: absolute;
|
||||
bottom: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, 50%);
|
||||
opacity: 50%;
|
||||
}
|
||||
|
||||
.barLabel {
|
||||
font-size: 22px;
|
||||
position: absolute;
|
||||
bottom: -10px;
|
||||
left: 50%;
|
||||
transform: translate(-50%, 100%);
|
||||
background-color: var(--background);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#mainContainer {
|
||||
visibility: hidden;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
|
||||
<link rel="stylesheet" href="./style.css" />
|
||||
<div id="mainContainer">
|
||||
<label id="goalLabel"></label>
|
||||
<label id="subCountLabel"></label>
|
||||
</div>
|
||||
|
||||
<script src="./script.js"></script>
|
||||
@@ -0,0 +1,36 @@
|
||||
////////////////////
|
||||
// URL PARAMETERS //
|
||||
////////////////////
|
||||
|
||||
const queryString = window.location.search;
|
||||
const urlParams = new URLSearchParams(queryString);
|
||||
const followMode = urlParams.has("followMode");
|
||||
const twitchUsername = urlParams.get("username") || '';
|
||||
const defaultGoalLabel = !followMode ? 'SUB GOAL' : 'FOLLOW GOAL'
|
||||
const goal = urlParams.get("goal") || defaultGoalLabel;
|
||||
|
||||
///////////////
|
||||
// FUNCTIONS //
|
||||
///////////////
|
||||
|
||||
async function UpdateMetrics() {
|
||||
document.getElementById("goalLabel").innerHTML = `${goal}: `;
|
||||
if (!followMode)
|
||||
document.getElementById("subCountLabel").innerHTML = await GetSubCount(`https://decapi.me/twitch/subcount`);
|
||||
else
|
||||
document.getElementById("subCountLabel").innerHTML = await GetSubCount(`https://decapi.me/twitch/followcount`);
|
||||
|
||||
setTimeout(UpdateMetrics, 10000);
|
||||
}
|
||||
|
||||
UpdateMetrics();
|
||||
|
||||
async function GetSubCount(url) {
|
||||
const response = await fetch(`${url}/${twitchUsername}`);
|
||||
const metric = await response.text();
|
||||
|
||||
if (metric.includes("decapi.me"))
|
||||
return "-";
|
||||
else
|
||||
return `${metric}/${parseInt(metric) + 1}`;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
#mainContainer {
|
||||
text-align: center;
|
||||
font-size: 40px;
|
||||
text-transform: uppercase;
|
||||
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica, Arial, sans-serif;
|
||||
|
||||
font-weight: 700;
|
||||
font-size: 36px;
|
||||
text-shadow: rgb(0, 0, 0) 2px 2px 2px;
|
||||
color: white;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
|
||||
<link rel="stylesheet" href="./style.css" />
|
||||
<div id="mainContainer">
|
||||
<label class="metricContainer">
|
||||
<label class="icon">★</label>
|
||||
<label id="subCountLabel"></label>
|
||||
</label>
|
||||
|
||||
<label class="metricContainer">
|
||||
<label class="icon">♥</label>
|
||||
<label class="metricLabel" id="followerCountLabel"></label>
|
||||
</label>
|
||||
|
||||
<label class="metricContainer">
|
||||
<label class="icon">👁</label>
|
||||
<label class="metricLabel" id="viewCountLabel"></label>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<script src="./script.js"></script>
|
||||
@@ -0,0 +1,38 @@
|
||||
////////////////////
|
||||
// URL PARAMETERS //
|
||||
////////////////////
|
||||
|
||||
const queryString = window.location.search;
|
||||
const urlParams = new URLSearchParams(queryString);
|
||||
const twitchUsername = urlParams.get("username") || '';
|
||||
|
||||
///////////////
|
||||
// FUNCTIONS //
|
||||
///////////////
|
||||
|
||||
async function UpdateMetrics() {
|
||||
document.getElementById("subCountLabel").innerHTML = await GetMetric("https://decapi.me/twitch/subcount");
|
||||
document.getElementById("followerCountLabel").innerHTML = await GetMetric("https://decapi.me/twitch/followcount");
|
||||
document.getElementById("viewCountLabel").innerHTML = await GetViewCount();
|
||||
|
||||
setTimeout(UpdateMetrics, 15000);
|
||||
}
|
||||
|
||||
UpdateMetrics();
|
||||
|
||||
async function GetMetric(url) {
|
||||
const response = await fetch(`${url}/${twitchUsername}`);
|
||||
const metric = await response.text();
|
||||
|
||||
if (metric.includes("decapi.me"))
|
||||
return "-";
|
||||
else
|
||||
return metric;
|
||||
}
|
||||
|
||||
async function GetViewCount() {
|
||||
const response = await fetch(`https://decapi.me/twitch/viewercount/${twitchUsername}`);
|
||||
const viewcount = await response.text();
|
||||
|
||||
return isNaN(Number(viewcount)) ? 0 : Number(viewcount);
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
#mainContainer {
|
||||
text-align: center;
|
||||
font-size: 40px;
|
||||
text-transform: uppercase;
|
||||
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica, Arial, sans-serif;
|
||||
|
||||
font-weight: 700;
|
||||
font-size: 36px;
|
||||
text-shadow: rgb(0, 0, 0) 2px 2px 2px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.metricContainer {
|
||||
padding: 0px 10px;
|
||||
}
|
||||
|
||||
.icon {
|
||||
font-size: 44px;
|
||||
padding: 0px 10px;
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<head>
|
||||
<title>Configure</title>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="shortcut icon" href="#" />
|
||||
<link rel="stylesheet" href="./style.css" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="mainContainer">
|
||||
<div id="connectBox">
|
||||
<div style="text-align: center;">
|
||||
<img src="logo.webp" width="60%">
|
||||
</div>
|
||||
<label>
|
||||
<br>
|
||||
<span style="font-weight: 500;">Client ID</span>
|
||||
<br>
|
||||
<input type="text" id="client_id_box">
|
||||
<br><br>
|
||||
<span style="font-weight: 500;">Client Secret</span>
|
||||
<br>
|
||||
<input type="password" id="client_secret_box">
|
||||
|
||||
<br><br>
|
||||
|
||||
<button id="authorizeButton" onclick="RequestAuthorization()">Authorize</button>
|
||||
<br><br>
|
||||
<button id="instructionsButton" onclick="OpenInstructions()">Instructions</button>
|
||||
</label>
|
||||
</div>
|
||||
<div id="authorizationBox">
|
||||
<label id="authorizationComplete">
|
||||
<img src="logo.webp" width="60%">
|
||||
<br>
|
||||
<br>
|
||||
<span style="font-weight: 500; font-size: 30px;">Authorization complete</span>
|
||||
<br><br>
|
||||
You now owe me money for all of my hard work.
|
||||
</label>
|
||||
<button id="copyURLButton" onclick="CopyToURL()">Click to copy URL</button>
|
||||
<br><br>
|
||||
<button id="donateButton" onclick="OpenDonationPage()">Donate out of guilt</button>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
<script src="./script.js"></script>
|
||||
|
After Width: | Height: | Size: 28 KiB |
@@ -0,0 +1,131 @@
|
||||
///////////////
|
||||
// PARAMETRS //
|
||||
///////////////
|
||||
|
||||
const queryString = window.location.search;
|
||||
const urlParams = new URLSearchParams(queryString);
|
||||
|
||||
const code = urlParams.get("code") || "";
|
||||
|
||||
const baseURL = "https://nuttylmao.github.io/spotify-widget-compact";
|
||||
const redirect_uri = `${baseURL}/configure`;
|
||||
let refresh_token = "";
|
||||
let access_token = "";
|
||||
let browserSourceURL = "";
|
||||
|
||||
|
||||
|
||||
/////////////////////////
|
||||
// AUTHORIZATION STUFF //
|
||||
/////////////////////////
|
||||
|
||||
function RequestAuthorization() {
|
||||
const client_id = document.getElementById("client_id_box").value;
|
||||
const client_secret = document.getElementById("client_secret_box").value;
|
||||
localStorage.setItem("client_id", client_id);
|
||||
localStorage.setItem("client_secret", client_secret);
|
||||
|
||||
let url = "https://accounts.spotify.com/authorize";
|
||||
url += "?client_id=" + client_id;
|
||||
url += "&response_type=code";
|
||||
url += "&redirect_uri=" + encodeURI(redirect_uri);
|
||||
url += "&show_dialog=true";
|
||||
url += "&scope=user-read-private user-read-email user-modify-playback-state user-read-playback-position user-library-read streaming user-read-playback-state user-read-recently-played playlist-read-private";
|
||||
window.location.href = url;
|
||||
}
|
||||
|
||||
// If there is no code in the query string, direct the user to authorize their account
|
||||
if (code != "") {
|
||||
FetchAccessToken(code);
|
||||
}
|
||||
else {
|
||||
document.getElementById("connectBox").style.display = 'inline';
|
||||
}
|
||||
|
||||
async function FetchAccessToken(code) {
|
||||
const client_id = localStorage.getItem("client_id");
|
||||
const client_secret = localStorage.getItem("client_secret");
|
||||
console.debug(`Client ID: ${client_id}`);
|
||||
console.debug(`Client Secret: ${client_secret}`);
|
||||
|
||||
let body = "grant_type=authorization_code";
|
||||
body += "&code=" + code;
|
||||
body += "&redirect_uri=" + encodeURI(redirect_uri);
|
||||
body += "&client_id=" + client_id;
|
||||
body += "&client_secret=" + client_secret;
|
||||
console.log(body);
|
||||
|
||||
// Get the current player information from Spotify
|
||||
const response = await fetch("https://accounts.spotify.com/api/token", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
'Authorization': `Basic ${btoa(client_id + ":" + client_secret)}`,
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
},
|
||||
body: body
|
||||
});
|
||||
|
||||
// If we got a response, save the access token
|
||||
if (response.ok)
|
||||
{
|
||||
const responseData = await response.json();
|
||||
console.debug(responseData);
|
||||
refresh_token = responseData.refresh_token; // Unsure if we need to replace the refresh_token but do it just in case
|
||||
access_token = responseData.access_token; // Save access token for all future API calls
|
||||
|
||||
browserSourceURL = `${baseURL}?client_id=${client_id}&client_secret=${client_secret}&refresh_token=${refresh_token}`;
|
||||
document.getElementById("authorizationBox").style.display = 'inline';
|
||||
}
|
||||
else
|
||||
{
|
||||
console.error(`${response.status}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////
|
||||
// BUTTON CLICK HANDLERS //
|
||||
///////////////////////////
|
||||
|
||||
const clientIdBox = document.getElementById('client_id_box');
|
||||
const clientSecretBox = document.getElementById('client_secret_box');
|
||||
const authorizeButton = document.getElementById('authorizeButton');
|
||||
|
||||
// Function to check if either input is empty
|
||||
function checkInputs() {
|
||||
if (clientIdBox.value.trim() === '' || clientSecretBox.value.trim() === '') {
|
||||
authorizeButton.disabled = true; // Disable the button
|
||||
} else {
|
||||
authorizeButton.disabled = false; // Enable the button
|
||||
}
|
||||
}
|
||||
|
||||
// Listen for changes in the input boxes
|
||||
clientIdBox.addEventListener('input', checkInputs);
|
||||
clientSecretBox.addEventListener('input', checkInputs);
|
||||
|
||||
// Initial check when the page loads, just in case
|
||||
checkInputs();
|
||||
|
||||
function CopyToURL() {
|
||||
navigator.clipboard.writeText(browserSourceURL);
|
||||
|
||||
document.getElementById("copyURLButton").innerText = "Copied to clipboard";
|
||||
document.getElementById("copyURLButton").style.backgroundColor = "#00dd63"
|
||||
document.getElementById("copyURLButton").style.color = "#ffffff";
|
||||
|
||||
setTimeout(() => {
|
||||
document.getElementById("copyURLButton").innerText = "Click to copy URL";
|
||||
document.getElementById("copyURLButton").style.backgroundColor = "#ffffff";
|
||||
document.getElementById("copyURLButton").style.color = "#181818";
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
function OpenInstructions() {
|
||||
window.open("https://nuttylmao.notion.site/Spotify-Widget-Compact-Edition-1ac19969b23780ae8c63c4472db0ee6b", '_blank').focus();
|
||||
}
|
||||
|
||||
function OpenDonationPage() {
|
||||
window.open("http://nutty.gg/pages/donate", '_blank').focus();
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
* {
|
||||
--corner-radius: 10px;
|
||||
--padding: 20px;
|
||||
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0px;
|
||||
color: white;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
#mainContainer {
|
||||
position: absolute;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
margin: 0px;
|
||||
background-color: #282828;
|
||||
}
|
||||
|
||||
#connectBox{
|
||||
background-color: #181818;
|
||||
border-radius: var(--corner-radius);
|
||||
padding: calc(var(--padding)*3) calc(var(--padding)*3);
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translateX(-50%) translateY(-50%);
|
||||
display: flex;
|
||||
filter: drop-shadow(0px 0px 4px rgba(0, 0, 0, 1));
|
||||
width: 400px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
#authorizationBox {
|
||||
background-color: #252525;
|
||||
border-radius: var(--corner-radius);
|
||||
padding: calc(var(--padding)*3) calc(var(--padding)*3);
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translateX(-50%) translateY(-50%);
|
||||
filter: drop-shadow(0px 0px 4px rgba(0, 0, 0, 1));
|
||||
width: 400px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
#authorizationComplete {
|
||||
display: block;
|
||||
text-align: center;
|
||||
padding-bottom: 40px;
|
||||
}
|
||||
|
||||
button {
|
||||
font-size: 24px;
|
||||
font-weight: 500;
|
||||
background-color: #3be477;
|
||||
color: white;
|
||||
opacity: 0.8;
|
||||
border-width: 0;
|
||||
border-radius: var(--corner-radius);
|
||||
padding: calc(var(--padding)/2) var(--padding);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
opacity: 1;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: inherit;
|
||||
}
|
||||
|
||||
#instructionsButton, #donateButton {
|
||||
background-color: #2e2e2e;
|
||||
}
|
||||
|
||||
#copyURLButton {
|
||||
background-color: #ffffff;
|
||||
color: #181818;
|
||||
width: 100%;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
#copyURLButton:disabled {
|
||||
background-color: #636363;
|
||||
}
|
||||
|
||||
input {
|
||||
border-radius: var(--corner-radius);
|
||||
width: calc(100% - 20px);
|
||||
font-size: 20px;
|
||||
margin: 10px 0px;
|
||||
padding: 10px 10px;
|
||||
background-color: #ffffff05;
|
||||
border-width: 0px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
textarea:focus, input:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
After Width: | Height: | Size: 62 KiB |
@@ -0,0 +1,46 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<head>
|
||||
<title>Spotify Widget</title>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="shortcut icon" href="#" />
|
||||
<link rel="stylesheet" href="./style.css" />
|
||||
<script src="https://cdn.socket.io/4.7.5/socket.io.min.js"
|
||||
integrity="sha384-2huaZvOR9iDzHqslqwpR87isEmrfxqyWOF7hr7BY6KG0+hVKLoEXMPUJw3ynWuhO"
|
||||
crossorigin="anonymous"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="statusContainer">Connecting...</div>
|
||||
<div id="mainContainer">
|
||||
<!-- <div id="albumArtBox">
|
||||
<img id="albumArt"
|
||||
src="https://static.wikia.nocookie.net/dreamcatcherwiki/images/d/dc/Dystopia_Lose_Myself_Digital_Cover.jpg"></img>
|
||||
<img id="albumArtBack"
|
||||
src="https://cdns-images.dzcdn.net/images/cover/762f7ef8a0c20e91304a3fae47449ccd/1900x1900-000000-80-0-0.jpg"></img>
|
||||
</div> -->
|
||||
<div id="songInfoBox">
|
||||
<div id="songInfo">
|
||||
<div id="IAmRunningOutOfNamesForTheseBoxes">
|
||||
<div id="songLabel">Can't get you out of my mind</div>
|
||||
<div id="artistLabel">Dreamcatcher</div>
|
||||
<!-- <div id="times">
|
||||
<div id="progressTime">2:29</div>
|
||||
<div id="timeRemaining">3:43</div>
|
||||
</div>
|
||||
<div id="progressBg">
|
||||
<div id="progressBar"></div>
|
||||
</div> -->
|
||||
</div>
|
||||
</div>
|
||||
<div id="backgroundArt">
|
||||
<img id="backgroundImage"
|
||||
src="https://static.wikia.nocookie.net/dreamcatcherwiki/images/d/dc/Dystopia_Lose_Myself_Digital_Cover.jpg"></img>
|
||||
<img id="backgroundImageBack"
|
||||
src="https://cdns-images.dzcdn.net/images/cover/762f7ef8a0c20e91304a3fae47449ccd/1900x1900-000000-80-0-0.jpg"></img>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
<script src="./script.js"></script>
|
||||
@@ -0,0 +1,270 @@
|
||||
///////////////
|
||||
// PARAMETRS //
|
||||
///////////////
|
||||
|
||||
const queryString = window.location.search;
|
||||
const urlParams = new URLSearchParams(queryString);
|
||||
|
||||
const client_id = urlParams.get("client_id") || "";
|
||||
const client_secret = urlParams.get("client_secret") || "";
|
||||
let refresh_token = urlParams.get("refresh_token") || "";
|
||||
let access_token = "";
|
||||
|
||||
const visibilityDuration = urlParams.get("duration") || 0;
|
||||
// const hideAlbumArt = urlParams.has("hideAlbumArt");
|
||||
|
||||
let currentState = false;
|
||||
let currentSongUri = "";
|
||||
|
||||
|
||||
|
||||
/////////////////
|
||||
// SPOTIFY API //
|
||||
/////////////////
|
||||
|
||||
// Update the access token - this expires so needs to be refreshed with refresh_token
|
||||
async function RefreshAccessToken() {
|
||||
console.debug(`Client ID: ${client_id}`);
|
||||
console.debug(`Client Secret: ${client_secret}`);
|
||||
console.debug(`Refresh Token: ${refresh_token}`);
|
||||
|
||||
let body = "grant_type=refresh_token";
|
||||
body += "&refresh_token=" + refresh_token;
|
||||
body += "&client_id=" + client_id;
|
||||
|
||||
const response = await fetch("https://accounts.spotify.com/api/token", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
'Authorization': `Basic ${btoa(client_id + ":" + client_secret)}`,
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
},
|
||||
body: body
|
||||
});
|
||||
|
||||
// If we got a response, save the access token
|
||||
if (response.ok)
|
||||
{
|
||||
const responseData = await response.json();
|
||||
console.debug(responseData);
|
||||
//refresh_token = responseData.refresh_token; // Unsure if we need to replace the refresh_token but do it just in case
|
||||
access_token = responseData.access_token; // Save access token for all future API calls
|
||||
}
|
||||
else
|
||||
{
|
||||
console.error(`${response.status}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function GetCurrentlyPlaying(refreshInterval) {
|
||||
try {
|
||||
// Get the current player information from Spotify
|
||||
const response = await fetch("https://api.spotify.com/v1/me/player/currently-playing", {
|
||||
method: "GET",
|
||||
headers: {
|
||||
'Authorization': `Bearer ${access_token}`,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
|
||||
// If we got a response, save the access token
|
||||
if (response.ok)
|
||||
{
|
||||
const responseData = await response.json();
|
||||
console.debug(responseData);
|
||||
UpdatePlayer(responseData);
|
||||
}
|
||||
else
|
||||
{
|
||||
switch (response.status)
|
||||
{
|
||||
case 401:
|
||||
console.debug(`${response.status}`)
|
||||
RefreshAccessToken();
|
||||
break;
|
||||
default:
|
||||
console.error(`${response.status}`)
|
||||
}
|
||||
}
|
||||
// Refresh
|
||||
setTimeout(() => {
|
||||
GetCurrentlyPlaying()
|
||||
}, 1000);
|
||||
}
|
||||
catch (error)
|
||||
{
|
||||
console.debug(error);
|
||||
SetVisibility(false);
|
||||
|
||||
// Try again in 2 seconds
|
||||
setTimeout(() => {
|
||||
GetCurrentlyPlaying()
|
||||
}, 2000);
|
||||
}
|
||||
}
|
||||
|
||||
function UpdatePlayer(data) {
|
||||
const isPlaying = data.is_playing; // The play/pause state of the player
|
||||
const songUri = data.item.uri;
|
||||
const albumArt = data.item.album.images.length > 0 ?
|
||||
`${data.item.album.images[0].url}`
|
||||
: `images/placeholder-album-art.png`; // The album art URL
|
||||
const artist = `${data.item.artists[0].name}`; // Name of the artist
|
||||
const name = `${data.item.name}`; // Name of the song
|
||||
const duration = `${data.item.duration_ms/1000}`; // The duration of the song in seconds
|
||||
const progress = `${data.progress_ms/1000}`; // The current position in seconds
|
||||
|
||||
// Set the visibility of the player, but only if the state is different than the last time we checked
|
||||
if (isPlaying != currentState) {
|
||||
|
||||
// Set player visibility
|
||||
if (!isPlaying)
|
||||
{
|
||||
console.debug("Hiding player...");
|
||||
SetVisibility(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
console.debug("Showing player...");
|
||||
setTimeout(() => {
|
||||
SetVisibility(true);
|
||||
|
||||
if (visibilityDuration > 0) {
|
||||
setTimeout(() => {
|
||||
SetVisibility(false, false);
|
||||
}, visibilityDuration * 1000);
|
||||
}
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
|
||||
if (songUri != currentSongUri) {
|
||||
if (isPlaying) {
|
||||
console.debug("Showing player...");
|
||||
setTimeout(() => {
|
||||
SetVisibility(true);
|
||||
|
||||
if (visibilityDuration > 0) {
|
||||
setTimeout(() => {
|
||||
SetVisibility(false, false);
|
||||
}, visibilityDuration * 1000);
|
||||
}
|
||||
}, 500);
|
||||
|
||||
currentSongUri = songUri;
|
||||
}
|
||||
}
|
||||
|
||||
// Set thumbnail
|
||||
// UpdateAlbumArt(document.getElementById("albumArt"), albumArt);
|
||||
UpdateAlbumArt(document.getElementById("backgroundImage"), albumArt);
|
||||
|
||||
// Set song info
|
||||
UpdateTextLabel(document.getElementById("artistLabel"), artist);
|
||||
UpdateTextLabel(document.getElementById("songLabel"), name);
|
||||
|
||||
// Set progressbar
|
||||
const progressPerc = ((progress / duration) * 100); // Progress expressed as a percentage
|
||||
const progressTime = ConvertSecondsToMinutesSoThatItLooksBetterOnTheOverlay(progress);
|
||||
const timeRemaining = ConvertSecondsToMinutesSoThatItLooksBetterOnTheOverlay(duration - progress);
|
||||
console.debug(`Progress: ${progressTime}`);
|
||||
console.debug(`Time Remaining: ${timeRemaining}`);
|
||||
// document.getElementById("progressBar").style.width = `${progressPerc}%`;
|
||||
// document.getElementById("progressTime").innerHTML = progressTime;
|
||||
// document.getElementById("timeRemaining").innerHTML = `-${timeRemaining}`;
|
||||
document.getElementById("backgroundImage").style.clipPath = `inset(0 ${100 - progressPerc}% 0 0)`;
|
||||
|
||||
setTimeout(() => {
|
||||
// document.getElementById("albumArtBack").src = albumArt;
|
||||
document.getElementById("backgroundImageBack").src = albumArt;
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
function UpdateTextLabel(div, text) {
|
||||
if (div.innerText != text) {
|
||||
div.setAttribute("class", "text-fade");
|
||||
setTimeout(() => {
|
||||
div.innerText = text;
|
||||
div.setAttribute("class", ".text-show");
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
|
||||
function UpdateAlbumArt(div, imgsrc) {
|
||||
if (div.src != imgsrc) {
|
||||
div.setAttribute("class", "text-fade");
|
||||
setTimeout(() => {
|
||||
div.src = imgsrc;
|
||||
div.setAttribute("class", "text-show");
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
//////////////////////
|
||||
// HELPER FUNCTIONS //
|
||||
//////////////////////
|
||||
|
||||
function ConvertSecondsToMinutesSoThatItLooksBetterOnTheOverlay(time) {
|
||||
const minutes = Math.floor(time / 60);
|
||||
const seconds = Math.trunc(time - minutes * 60);
|
||||
|
||||
return `${minutes}:${('0' + seconds).slice(-2)}`;
|
||||
}
|
||||
|
||||
function SetVisibility(isVisible, updateCurrentState = true) {
|
||||
widgetVisibility = isVisible;
|
||||
|
||||
const mainContainer = document.getElementById("mainContainer");
|
||||
|
||||
if (isVisible) {
|
||||
mainContainer.style.opacity = 1;
|
||||
mainContainer.style.bottom = "50%";
|
||||
}
|
||||
else {
|
||||
mainContainer.style.opacity = 0;
|
||||
mainContainer.style.bottom = "calc(50% - 20px)";
|
||||
}
|
||||
|
||||
if (updateCurrentState)
|
||||
currentState = isVisible;
|
||||
}
|
||||
|
||||
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////
|
||||
// RESIZER THING BECAUSE I THINK I KNOW HOW RESPONSIVE DESIGN WORKS EVEN THOUGH I DON'T //
|
||||
//////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
let outer = document.getElementById('mainContainer'),
|
||||
maxWidth = outer.clientWidth+100,
|
||||
maxHeight = outer.clientHeight;
|
||||
|
||||
window.addEventListener("resize", resize);
|
||||
|
||||
resize();
|
||||
function resize() {
|
||||
const scale = window.innerWidth / maxWidth;
|
||||
outer.style.transform = 'translate(-50%, 50%) scale(' + scale + ')';
|
||||
}
|
||||
|
||||
|
||||
|
||||
/////////////////////////////////////////////////////////////////////
|
||||
// IF THE USER PUT IN THE HIDEALBUMART PARAMATER, THEN YOU SHOULD //
|
||||
// HIDE THE ALBUM ART, BECAUSE THAT'S WHAT IT'S SUPPOSED TO DO //
|
||||
/////////////////////////////////////////////////////////////////////
|
||||
|
||||
// if (hideAlbumArt) {
|
||||
// document.getElementById("albumArtBox").style.display = "none";
|
||||
// document.getElementById("songInfoBox").style.width = "calc(100% - 20px)";
|
||||
// }
|
||||
|
||||
|
||||
|
||||
////////////////////////////////
|
||||
// KICK OFF THE WHOLE WIDGET //
|
||||
////////////////////////////////
|
||||
|
||||
RefreshAccessToken();
|
||||
GetCurrentlyPlaying(); // This is a recursive function, so just run it once
|
||||
@@ -0,0 +1,191 @@
|
||||
* {
|
||||
--corner-radius: 25px;
|
||||
--album-art-size: 50px;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
#statusContainer {
|
||||
font-weight: 500;
|
||||
font-size: 30px;
|
||||
text-align: center;
|
||||
background-color: #D12025;
|
||||
color: white;
|
||||
padding: 10px;
|
||||
border-radius: 10px;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
#mainContainer {
|
||||
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica, Arial, sans-serif;
|
||||
display: flex;
|
||||
position: absolute;
|
||||
height: var(--album-art-size);
|
||||
margin: 20px;
|
||||
filter: drop-shadow(15px 15px 7px rgba(0, 0, 0, 1));
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
bottom: calc(50% - 20px);
|
||||
left: 50%;
|
||||
opacity: 0;
|
||||
transition: all 0.5s ease;
|
||||
}
|
||||
|
||||
/* #albumArtBox {
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
position: relative;
|
||||
border-radius: var(--corner-radius);
|
||||
overflow: hidden;
|
||||
margin: 0px 8px 0px 0px;
|
||||
object-fit: cover;
|
||||
width: var(--album-art-size);
|
||||
}
|
||||
|
||||
#albumArt {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
top: 0%;
|
||||
object-fit: cover;
|
||||
}
|
||||
#albumArtBack {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
} */
|
||||
|
||||
#songInfoBox {
|
||||
position: relative;
|
||||
color: white;
|
||||
/* width: calc(100% - 125px); */
|
||||
width: calc(100%);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 0 1 auto;
|
||||
justify-content: center;
|
||||
z-index: 1;
|
||||
text-shadow: 2px 2px 2px rgba(0, 0, 0, 0.5);
|
||||
overflow: hidden;
|
||||
z-index: 4;
|
||||
}
|
||||
|
||||
#songInfo {
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
opacity: 0.9;
|
||||
position: relative;
|
||||
border-radius: var(--corner-radius);
|
||||
padding: 0px 20px;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#IAmRunningOutOfNamesForTheseBoxes {
|
||||
position: absolute;
|
||||
width: calc(100% - 40px);
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
-ms-transform: translateY(-50%);
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
#backgroundArt {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
border-radius: var(--corner-radius);
|
||||
overflow: hidden;
|
||||
z-index: -1;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
#backgroundImage {
|
||||
filter: blur(20px);
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%) translateY(-50%);
|
||||
transition: all 2s ease;
|
||||
}
|
||||
|
||||
#backgroundImageBack {
|
||||
filter: blur(20px) grayscale(75%);
|
||||
opacity: 0.5;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%) translateY(-50%);
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
#artistLabel {
|
||||
font-size: 16px;
|
||||
font-weight: 400;
|
||||
line-height: 1;
|
||||
white-space: nowrap;
|
||||
transition: all 0.5s ease;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#songLabel {
|
||||
font-weight: bold;
|
||||
font-size: 20px;
|
||||
width: calc(100%);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
transition: all 0.5s ease;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* #progressBg {
|
||||
margin-top: 15px;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
border-radius: 5px;
|
||||
background-color: #1F1F1F;
|
||||
} */
|
||||
|
||||
/* #progressBar {
|
||||
border-radius: 5px;
|
||||
height: 5px;
|
||||
width: 20%;
|
||||
background-color: #ffffff;
|
||||
margin: 10px 0px;
|
||||
transition: all 1s ease;
|
||||
} */
|
||||
|
||||
/* #times {
|
||||
position: relative;
|
||||
height: 16px;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
line-height: 2.2;
|
||||
} */
|
||||
|
||||
/* #progressTime {
|
||||
position: absolute;
|
||||
} */
|
||||
|
||||
/* #timeRemaining {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
text-align: right;
|
||||
} */
|
||||
|
||||
.text-show {
|
||||
opacity: 1;
|
||||
transition: all 0.25s ease;
|
||||
}
|
||||
|
||||
.text-fade {
|
||||
opacity: 0;
|
||||
transition: all 0.25s ease;
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<head>
|
||||
<title>Configure</title>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="shortcut icon" href="#" />
|
||||
<link rel="stylesheet" href="./style.css" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="mainContainer">
|
||||
<div id="connectBox">
|
||||
<div style="text-align: center;">
|
||||
<img src="logo.webp" width="60%">
|
||||
</div>
|
||||
<label>
|
||||
<br>
|
||||
<span style="font-weight: 500;">Client ID</span>
|
||||
<br>
|
||||
<input type="text" id="client_id_box">
|
||||
<br><br>
|
||||
<span style="font-weight: 500;">Client Secret</span>
|
||||
<br>
|
||||
<input type="password" id="client_secret_box">
|
||||
|
||||
<br><br>
|
||||
|
||||
<button id="authorizeButton" onclick="RequestAuthorization()">Authorize</button>
|
||||
<br><br>
|
||||
<button id="instructionsButton" onclick="OpenInstructions()">Instructions</button>
|
||||
</label>
|
||||
</div>
|
||||
<div id="authorizationBox">
|
||||
<label id="authorizationComplete">
|
||||
<img src="logo.webp" width="60%">
|
||||
<br>
|
||||
<br>
|
||||
<span style="font-weight: 500; font-size: 30px;">Authorization complete</span>
|
||||
<br><br>
|
||||
You now owe me money for all of my hard work.
|
||||
</label>
|
||||
<button id="copyURLButton" onclick="CopyToURL()">Click to copy URL</button>
|
||||
<br><br>
|
||||
<button id="donateButton" onclick="OpenDonationPage()">Donate out of guilt</button>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
<script src="./script.js"></script>
|
||||
|
After Width: | Height: | Size: 28 KiB |
@@ -0,0 +1,131 @@
|
||||
///////////////
|
||||
// PARAMETRS //
|
||||
///////////////
|
||||
|
||||
const queryString = window.location.search;
|
||||
const urlParams = new URLSearchParams(queryString);
|
||||
|
||||
const code = urlParams.get("code") || "";
|
||||
|
||||
const baseURL = "https://nuttylmao.github.io/spotify-widget";
|
||||
const redirect_uri = `${baseURL}/configure`;
|
||||
let refresh_token = "";
|
||||
let access_token = "";
|
||||
let browserSourceURL = "";
|
||||
|
||||
|
||||
|
||||
/////////////////////////
|
||||
// AUTHORIZATION STUFF //
|
||||
/////////////////////////
|
||||
|
||||
function RequestAuthorization() {
|
||||
const client_id = document.getElementById("client_id_box").value;
|
||||
const client_secret = document.getElementById("client_secret_box").value;
|
||||
localStorage.setItem("client_id", client_id);
|
||||
localStorage.setItem("client_secret", client_secret);
|
||||
|
||||
let url = "https://accounts.spotify.com/authorize";
|
||||
url += "?client_id=" + client_id;
|
||||
url += "&response_type=code";
|
||||
url += "&redirect_uri=" + encodeURI(redirect_uri);
|
||||
url += "&show_dialog=true";
|
||||
url += "&scope=user-read-private user-read-email user-modify-playback-state user-read-playback-position user-library-read streaming user-read-playback-state user-read-recently-played playlist-read-private";
|
||||
window.location.href = url;
|
||||
}
|
||||
|
||||
// If there is no code in the query string, direct the user to authorize their account
|
||||
if (code != "") {
|
||||
FetchAccessToken(code);
|
||||
}
|
||||
else {
|
||||
document.getElementById("connectBox").style.display = 'inline';
|
||||
}
|
||||
|
||||
async function FetchAccessToken(code) {
|
||||
const client_id = localStorage.getItem("client_id");
|
||||
const client_secret = localStorage.getItem("client_secret");
|
||||
console.debug(`Client ID: ${client_id}`);
|
||||
console.debug(`Client Secret: ${client_secret}`);
|
||||
|
||||
let body = "grant_type=authorization_code";
|
||||
body += "&code=" + code;
|
||||
body += "&redirect_uri=" + encodeURI(redirect_uri);
|
||||
body += "&client_id=" + client_id;
|
||||
body += "&client_secret=" + client_secret;
|
||||
console.log(body);
|
||||
|
||||
// Get the current player information from Spotify
|
||||
const response = await fetch("https://accounts.spotify.com/api/token", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
'Authorization': `Basic ${btoa(client_id + ":" + client_secret)}`,
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
},
|
||||
body: body
|
||||
});
|
||||
|
||||
// If we got a response, save the access token
|
||||
if (response.ok)
|
||||
{
|
||||
const responseData = await response.json();
|
||||
console.debug(responseData);
|
||||
refresh_token = responseData.refresh_token; // Unsure if we need to replace the refresh_token but do it just in case
|
||||
access_token = responseData.access_token; // Save access token for all future API calls
|
||||
|
||||
browserSourceURL = `${baseURL}?client_id=${client_id}&client_secret=${client_secret}&refresh_token=${refresh_token}`;
|
||||
document.getElementById("authorizationBox").style.display = 'inline';
|
||||
}
|
||||
else
|
||||
{
|
||||
console.error(`${response.status}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////
|
||||
// BUTTON CLICK HANDLERS //
|
||||
///////////////////////////
|
||||
|
||||
const clientIdBox = document.getElementById('client_id_box');
|
||||
const clientSecretBox = document.getElementById('client_secret_box');
|
||||
const authorizeButton = document.getElementById('authorizeButton');
|
||||
|
||||
// Function to check if either input is empty
|
||||
function checkInputs() {
|
||||
if (clientIdBox.value.trim() === '' || clientSecretBox.value.trim() === '') {
|
||||
authorizeButton.disabled = true; // Disable the button
|
||||
} else {
|
||||
authorizeButton.disabled = false; // Enable the button
|
||||
}
|
||||
}
|
||||
|
||||
// Listen for changes in the input boxes
|
||||
clientIdBox.addEventListener('input', checkInputs);
|
||||
clientSecretBox.addEventListener('input', checkInputs);
|
||||
|
||||
// Initial check when the page loads, just in case
|
||||
checkInputs();
|
||||
|
||||
function CopyToURL() {
|
||||
navigator.clipboard.writeText(browserSourceURL);
|
||||
|
||||
document.getElementById("copyURLButton").innerText = "Copied to clipboard";
|
||||
document.getElementById("copyURLButton").style.backgroundColor = "#00dd63"
|
||||
document.getElementById("copyURLButton").style.color = "#ffffff";
|
||||
|
||||
setTimeout(() => {
|
||||
document.getElementById("copyURLButton").innerText = "Click to copy URL";
|
||||
document.getElementById("copyURLButton").style.backgroundColor = "#ffffff";
|
||||
document.getElementById("copyURLButton").style.color = "#181818";
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
function OpenInstructions() {
|
||||
window.open("https://nuttylmao.notion.site/Spotify-Widget-18e19969b237807ca88cfc9c4159da15", '_blank').focus();
|
||||
}
|
||||
|
||||
function OpenDonationPage() {
|
||||
window.open("http://nutty.gg/pages/donate", '_blank').focus();
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
* {
|
||||
--corner-radius: 10px;
|
||||
--padding: 20px;
|
||||
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0px;
|
||||
color: white;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
#mainContainer {
|
||||
position: absolute;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
margin: 0px;
|
||||
background-color: #282828;
|
||||
}
|
||||
|
||||
#connectBox{
|
||||
background-color: #181818;
|
||||
border-radius: var(--corner-radius);
|
||||
padding: calc(var(--padding)*3) calc(var(--padding)*3);
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translateX(-50%) translateY(-50%);
|
||||
display: flex;
|
||||
filter: drop-shadow(0px 0px 4px rgba(0, 0, 0, 1));
|
||||
width: 400px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
#authorizationBox {
|
||||
background-color: #252525;
|
||||
border-radius: var(--corner-radius);
|
||||
padding: calc(var(--padding)*3) calc(var(--padding)*3);
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translateX(-50%) translateY(-50%);
|
||||
filter: drop-shadow(0px 0px 4px rgba(0, 0, 0, 1));
|
||||
width: 400px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
#authorizationComplete {
|
||||
display: block;
|
||||
text-align: center;
|
||||
padding-bottom: 40px;
|
||||
}
|
||||
|
||||
button {
|
||||
font-size: 24px;
|
||||
font-weight: 500;
|
||||
background-color: #3be477;
|
||||
color: white;
|
||||
opacity: 0.8;
|
||||
border-width: 0;
|
||||
border-radius: var(--corner-radius);
|
||||
padding: calc(var(--padding)/2) var(--padding);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
opacity: 1;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: inherit;
|
||||
}
|
||||
|
||||
#instructionsButton, #donateButton {
|
||||
background-color: #2e2e2e;
|
||||
}
|
||||
|
||||
#copyURLButton {
|
||||
background-color: #ffffff;
|
||||
color: #181818;
|
||||
width: 100%;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
#copyURLButton:disabled {
|
||||
background-color: #636363;
|
||||
}
|
||||
|
||||
input {
|
||||
border-radius: var(--corner-radius);
|
||||
width: calc(100% - 20px);
|
||||
font-size: 20px;
|
||||
margin: 10px 0px;
|
||||
padding: 10px 10px;
|
||||
background-color: #ffffff05;
|
||||
border-width: 0px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
textarea:focus, input:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
After Width: | Height: | Size: 62 KiB |
@@ -0,0 +1,46 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<head>
|
||||
<title>Spotify Widget</title>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="shortcut icon" href="#" />
|
||||
<link rel="stylesheet" href="./style.css" />
|
||||
<script src="https://cdn.socket.io/4.7.5/socket.io.min.js"
|
||||
integrity="sha384-2huaZvOR9iDzHqslqwpR87isEmrfxqyWOF7hr7BY6KG0+hVKLoEXMPUJw3ynWuhO"
|
||||
crossorigin="anonymous"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="statusContainer">Connecting...</div>
|
||||
<div id="mainContainer">
|
||||
<div id="albumArtBox">
|
||||
<img id="albumArt"
|
||||
src="https://static.wikia.nocookie.net/dreamcatcherwiki/images/d/dc/Dystopia_Lose_Myself_Digital_Cover.jpg"></img>
|
||||
<img id="albumArtBack"
|
||||
src="https://cdns-images.dzcdn.net/images/cover/762f7ef8a0c20e91304a3fae47449ccd/1900x1900-000000-80-0-0.jpg"></img>
|
||||
</div>
|
||||
<div id="songInfoBox">
|
||||
<div id="songInfo">
|
||||
<div id="IAmRunningOutOfNamesForTheseBoxes">
|
||||
<div id="songLabel">Can't get you out of my mind</div>
|
||||
<div id="artistLabel">Dreamcatcher</div>
|
||||
<div id="times">
|
||||
<div id="progressTime">2:29</div>
|
||||
<div id="timeRemaining">3:43</div>
|
||||
</div>
|
||||
<div id="progressBg">
|
||||
<div id="progressBar"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="backgroundArt">
|
||||
<img id="backgroundImage"
|
||||
src="https://static.wikia.nocookie.net/dreamcatcherwiki/images/d/dc/Dystopia_Lose_Myself_Digital_Cover.jpg"></img>
|
||||
<img id="backgroundImageBack"
|
||||
src="https://cdns-images.dzcdn.net/images/cover/762f7ef8a0c20e91304a3fae47449ccd/1900x1900-000000-80-0-0.jpg"></img>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
<script src="./script.js"></script>
|
||||
@@ -0,0 +1,269 @@
|
||||
///////////////
|
||||
// PARAMETRS //
|
||||
///////////////
|
||||
|
||||
const queryString = window.location.search;
|
||||
const urlParams = new URLSearchParams(queryString);
|
||||
|
||||
const client_id = urlParams.get("client_id") || "";
|
||||
const client_secret = urlParams.get("client_secret") || "";
|
||||
let refresh_token = urlParams.get("refresh_token") || "";
|
||||
let access_token = "";
|
||||
|
||||
const visibilityDuration = urlParams.get("duration") || 0;
|
||||
const hideAlbumArt = urlParams.has("hideAlbumArt");
|
||||
|
||||
let currentState = false;
|
||||
let currentSongUri = "";
|
||||
|
||||
|
||||
|
||||
/////////////////
|
||||
// SPOTIFY API //
|
||||
/////////////////
|
||||
|
||||
// Update the access token - this expires so needs to be refreshed with refresh_token
|
||||
async function RefreshAccessToken() {
|
||||
console.debug(`Client ID: ${client_id}`);
|
||||
console.debug(`Client Secret: ${client_secret}`);
|
||||
console.debug(`Refresh Token: ${refresh_token}`);
|
||||
|
||||
let body = "grant_type=refresh_token";
|
||||
body += "&refresh_token=" + refresh_token;
|
||||
body += "&client_id=" + client_id;
|
||||
|
||||
const response = await fetch("https://accounts.spotify.com/api/token", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
'Authorization': `Basic ${btoa(client_id + ":" + client_secret)}`,
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
},
|
||||
body: body
|
||||
});
|
||||
|
||||
// If we got a response, save the access token
|
||||
if (response.ok)
|
||||
{
|
||||
const responseData = await response.json();
|
||||
console.debug(responseData);
|
||||
//refresh_token = responseData.refresh_token; // Unsure if we need to replace the refresh_token but do it just in case
|
||||
access_token = responseData.access_token; // Save access token for all future API calls
|
||||
}
|
||||
else
|
||||
{
|
||||
console.error(`${response.status}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function GetCurrentlyPlaying(refreshInterval) {
|
||||
try {
|
||||
// Get the current player information from Spotify
|
||||
const response = await fetch("https://api.spotify.com/v1/me/player/currently-playing", {
|
||||
method: "GET",
|
||||
headers: {
|
||||
'Authorization': `Bearer ${access_token}`,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
|
||||
// If we got a response, save the access token
|
||||
if (response.ok)
|
||||
{
|
||||
const responseData = await response.json();
|
||||
console.debug(responseData);
|
||||
UpdatePlayer(responseData);
|
||||
}
|
||||
else
|
||||
{
|
||||
switch (response.status)
|
||||
{
|
||||
case 401:
|
||||
console.debug(`${response.status}`)
|
||||
RefreshAccessToken();
|
||||
break;
|
||||
default:
|
||||
console.error(`${response.status}`)
|
||||
}
|
||||
}
|
||||
// Refresh
|
||||
setTimeout(() => {
|
||||
GetCurrentlyPlaying()
|
||||
}, 1000);
|
||||
}
|
||||
catch (error)
|
||||
{
|
||||
console.debug(error);
|
||||
SetVisibility(false);
|
||||
|
||||
// Try again in 2 seconds
|
||||
setTimeout(() => {
|
||||
GetCurrentlyPlaying()
|
||||
}, 2000);
|
||||
}
|
||||
}
|
||||
|
||||
function UpdatePlayer(data) {
|
||||
const isPlaying = data.is_playing; // The play/pause state of the player
|
||||
const songUri = data.item.uri;
|
||||
const albumArt = data.item.album.images.length > 0 ?
|
||||
`${data.item.album.images[0].url}`
|
||||
: `images/placeholder-album-art.png`; // The album art URL
|
||||
const artist = `${data.item.artists[0].name}`; // Name of the artist
|
||||
const name = `${data.item.name}`; // Name of the song
|
||||
const duration = `${data.item.duration_ms/1000}`; // The duration of the song in seconds
|
||||
const progress = `${data.progress_ms/1000}`; // The current position in seconds
|
||||
|
||||
// Set the visibility of the player, but only if the state is different than the last time we checked
|
||||
if (isPlaying != currentState) {
|
||||
|
||||
// Set player visibility
|
||||
if (!isPlaying)
|
||||
{
|
||||
console.debug("Hiding player...");
|
||||
SetVisibility(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
console.debug("Showing player...");
|
||||
setTimeout(() => {
|
||||
SetVisibility(true);
|
||||
|
||||
if (visibilityDuration > 0) {
|
||||
setTimeout(() => {
|
||||
SetVisibility(false, false);
|
||||
}, visibilityDuration * 1000);
|
||||
}
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
|
||||
if (songUri != currentSongUri) {
|
||||
if (isPlaying) {
|
||||
console.debug("Showing player...");
|
||||
setTimeout(() => {
|
||||
SetVisibility(true);
|
||||
|
||||
if (visibilityDuration > 0) {
|
||||
setTimeout(() => {
|
||||
SetVisibility(false, false);
|
||||
}, visibilityDuration * 1000);
|
||||
}
|
||||
}, 500);
|
||||
|
||||
currentSongUri = songUri;
|
||||
}
|
||||
}
|
||||
|
||||
// Set thumbnail
|
||||
UpdateAlbumArt(document.getElementById("albumArt"), albumArt);
|
||||
UpdateAlbumArt(document.getElementById("backgroundImage"), albumArt);
|
||||
|
||||
// Set song info
|
||||
UpdateTextLabel(document.getElementById("artistLabel"), artist);
|
||||
UpdateTextLabel(document.getElementById("songLabel"), name);
|
||||
|
||||
// Set progressbar
|
||||
const progressPerc = ((progress / duration) * 100); // Progress expressed as a percentage
|
||||
const progressTime = ConvertSecondsToMinutesSoThatItLooksBetterOnTheOverlay(progress);
|
||||
const timeRemaining = ConvertSecondsToMinutesSoThatItLooksBetterOnTheOverlay(duration - progress);
|
||||
console.debug(`Progress: ${progressTime}`);
|
||||
console.debug(`Time Remaining: ${timeRemaining}`);
|
||||
document.getElementById("progressBar").style.width = `${progressPerc}%`;
|
||||
document.getElementById("progressTime").innerHTML = progressTime;
|
||||
document.getElementById("timeRemaining").innerHTML = `-${timeRemaining}`;
|
||||
|
||||
setTimeout(() => {
|
||||
document.getElementById("albumArtBack").src = albumArt;
|
||||
document.getElementById("backgroundImageBack").src = albumArt;
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
function UpdateTextLabel(div, text) {
|
||||
if (div.innerText != text) {
|
||||
div.setAttribute("class", "text-fade");
|
||||
setTimeout(() => {
|
||||
div.innerText = text;
|
||||
div.setAttribute("class", ".text-show");
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
|
||||
function UpdateAlbumArt(div, imgsrc) {
|
||||
if (div.src != imgsrc) {
|
||||
div.setAttribute("class", "text-fade");
|
||||
setTimeout(() => {
|
||||
div.src = imgsrc;
|
||||
div.setAttribute("class", "text-show");
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
//////////////////////
|
||||
// HELPER FUNCTIONS //
|
||||
//////////////////////
|
||||
|
||||
function ConvertSecondsToMinutesSoThatItLooksBetterOnTheOverlay(time) {
|
||||
const minutes = Math.floor(time / 60);
|
||||
const seconds = Math.trunc(time - minutes * 60);
|
||||
|
||||
return `${minutes}:${('0' + seconds).slice(-2)}`;
|
||||
}
|
||||
|
||||
function SetVisibility(isVisible, updateCurrentState = true) {
|
||||
widgetVisibility = isVisible;
|
||||
|
||||
const mainContainer = document.getElementById("mainContainer");
|
||||
|
||||
if (isVisible) {
|
||||
mainContainer.style.opacity = 1;
|
||||
mainContainer.style.bottom = "50%";
|
||||
}
|
||||
else {
|
||||
mainContainer.style.opacity = 0;
|
||||
mainContainer.style.bottom = "calc(50% - 20px)";
|
||||
}
|
||||
|
||||
if (updateCurrentState)
|
||||
currentState = isVisible;
|
||||
}
|
||||
|
||||
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////
|
||||
// RESIZER THING BECAUSE I THINK I KNOW HOW RESPONSIVE DESIGN WORKS EVEN THOUGH I DON'T //
|
||||
//////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
let outer = document.getElementById('mainContainer'),
|
||||
maxWidth = outer.clientWidth+50,
|
||||
maxHeight = outer.clientHeight;
|
||||
|
||||
window.addEventListener("resize", resize);
|
||||
|
||||
resize();
|
||||
function resize() {
|
||||
const scale = window.innerWidth / maxWidth;
|
||||
outer.style.transform = 'translate(-50%, 50%) scale(' + scale + ')';
|
||||
}
|
||||
|
||||
|
||||
|
||||
/////////////////////////////////////////////////////////////////////
|
||||
// IF THE USER PUT IN THE HIDEALBUMART PARAMATER, THEN YOU SHOULD //
|
||||
// HIDE THE ALBUM ART, BECAUSE THAT'S WHAT IT'S SUPPOSED TO DO //
|
||||
/////////////////////////////////////////////////////////////////////
|
||||
|
||||
if (hideAlbumArt) {
|
||||
document.getElementById("albumArtBox").style.display = "none";
|
||||
document.getElementById("songInfoBox").style.width = "calc(100% - 20px)";
|
||||
}
|
||||
|
||||
|
||||
|
||||
////////////////////////////////
|
||||
// KICK OFF THE WHOLE WIDGET //
|
||||
////////////////////////////////
|
||||
|
||||
RefreshAccessToken();
|
||||
GetCurrentlyPlaying(); // This is a recursive function, so just run it once
|
||||
@@ -0,0 +1,192 @@
|
||||
* {
|
||||
--corner-radius: 10px;
|
||||
--album-art-size: 100px;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
#statusContainer {
|
||||
font-weight: 500;
|
||||
font-size: 30px;
|
||||
text-align: center;
|
||||
background-color: #D12025;
|
||||
color: white;
|
||||
padding: 10px;
|
||||
border-radius: 10px;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
#mainContainer {
|
||||
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica, Arial, sans-serif;
|
||||
display: flex;
|
||||
position: absolute;
|
||||
height: var(--album-art-size);
|
||||
margin: 20px;
|
||||
filter: drop-shadow(15px 15px 7px rgba(0, 0, 0, 1));
|
||||
width: 100%;
|
||||
max-width: 500px;
|
||||
bottom: calc(50% - 20px);
|
||||
left: 50%;
|
||||
opacity: 0;
|
||||
transition: all 0.5s ease;
|
||||
}
|
||||
|
||||
#albumArtBox {
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
position: relative;
|
||||
border-radius: var(--corner-radius);
|
||||
overflow: hidden;
|
||||
margin: 0px 8px 0px 0px;
|
||||
object-fit: cover;
|
||||
width: var(--album-art-size);
|
||||
}
|
||||
|
||||
#albumArt {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
top: 0%;
|
||||
object-fit: cover;
|
||||
}
|
||||
#albumArtBack {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
#albumArtBox img {
|
||||
/* width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover; */
|
||||
}
|
||||
|
||||
#songInfoBox {
|
||||
position: relative;
|
||||
color: white;
|
||||
width: calc(100% - 125px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 0 1 auto;
|
||||
justify-content: center;
|
||||
z-index: 1;
|
||||
text-shadow: 2px 2px 2px rgba(0, 0, 0, 0.5);
|
||||
overflow: hidden;
|
||||
z-index: 4;
|
||||
}
|
||||
|
||||
#songInfo {
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
opacity: 0.9;
|
||||
position: relative;
|
||||
border-radius: var(--corner-radius);
|
||||
padding: 0px 20px;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#IAmRunningOutOfNamesForTheseBoxes {
|
||||
position: absolute;
|
||||
width: calc(100% - 40px);
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
-ms-transform: translateY(-50%);
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
#backgroundArt {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
border-radius: var(--corner-radius);
|
||||
overflow: hidden;
|
||||
z-index: -1;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
#backgroundImage {
|
||||
filter: blur(20px);
|
||||
position: absolute;
|
||||
width: 140%;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%) translateY(-50%);
|
||||
}
|
||||
|
||||
#backgroundImageBack {
|
||||
filter: blur(20px);
|
||||
position: absolute;
|
||||
width: 140%;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%) translateY(-50%);
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
#artistLabel {
|
||||
font-size: 16px;
|
||||
font-weight: 400;
|
||||
line-height: 1;
|
||||
white-space: nowrap;
|
||||
transition: all 0.5s ease;
|
||||
}
|
||||
|
||||
#songLabel {
|
||||
font-weight: bold;
|
||||
font-size: 20px;
|
||||
width: calc(100%);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
transition: all 0.5s ease;
|
||||
}
|
||||
|
||||
#progressBg {
|
||||
margin-top: 15px;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
border-radius: 5px;
|
||||
background-color: #1F1F1F;
|
||||
}
|
||||
|
||||
#progressBar {
|
||||
border-radius: 5px;
|
||||
height: 5px;
|
||||
width: 20%;
|
||||
background-color: #ffffff;
|
||||
margin: 10px 0px;
|
||||
transition: all 1s ease;
|
||||
}
|
||||
|
||||
#times {
|
||||
position: relative;
|
||||
height: 16px;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
line-height: 2.2;
|
||||
}
|
||||
|
||||
#progressTime {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
#timeRemaining {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.text-show {
|
||||
opacity: 1;
|
||||
transition: all 0.25s ease;
|
||||
}
|
||||
|
||||
.text-fade {
|
||||
opacity: 0;
|
||||
transition: all 0.25s ease;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
|
||||
<link rel="stylesheet" href="./style.css" />
|
||||
<div id="mainContainer">
|
||||
<label id="uptimeLabel"></label>
|
||||
</div>
|
||||
|
||||
<script src="./script.js"></script>
|
||||
@@ -0,0 +1,40 @@
|
||||
////////////////////
|
||||
// URL PARAMETERS //
|
||||
////////////////////
|
||||
|
||||
const queryString = window.location.search;
|
||||
const urlParams = new URLSearchParams(queryString);
|
||||
const twitchUsername = urlParams.get("username") || '';
|
||||
|
||||
///////////////
|
||||
// FUNCTIONS //
|
||||
///////////////
|
||||
|
||||
async function UpdateUptime() {
|
||||
document.getElementById("uptimeLabel").innerHTML = await GetUptime();
|
||||
|
||||
setTimeout(UpdateUptime, 30000);
|
||||
}
|
||||
|
||||
UpdateUptime();
|
||||
|
||||
async function GetUptime() {
|
||||
const response = await fetch(`https://decapi.me/twitch/uptime/${twitchUsername}`);
|
||||
const metric = await response.text();
|
||||
|
||||
if (metric.includes("second"))
|
||||
{
|
||||
return removeTextAfterLastComma(metric);
|
||||
}
|
||||
else
|
||||
return metric;
|
||||
}
|
||||
|
||||
function removeTextAfterLastComma(str) {
|
||||
const lastCommaIndex = str.lastIndexOf(',');
|
||||
if (lastCommaIndex !== -1) {
|
||||
return str.substring(0, lastCommaIndex);
|
||||
} else {
|
||||
return str; // No comma found, return the original string
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
#mainContainer {
|
||||
text-align: center;
|
||||
font-size: 40px;
|
||||
text-transform: uppercase;
|
||||
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica, Arial, sans-serif;
|
||||
|
||||
font-weight: 700;
|
||||
font-size: 36px;
|
||||
text-shadow: rgb(0, 0, 0) 2px 2px 2px;
|
||||
color: white;
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Settings</title>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<div id="pinned-header">
|
||||
<h2>Widget URL</h2>
|
||||
<input id="widget-url" type="text" readonly>
|
||||
<button id="save-settings">Click to copy URL</button>
|
||||
<button onclick="OpenMembershipPage()">💎 Member Exclusive Widgets</button>
|
||||
</div>
|
||||
|
||||
<div id="settings-container">
|
||||
<div id="settings-content"></div>
|
||||
</div>
|
||||
<script src="script.js"></script>
|
||||
|
||||
<div id="mommy-milkers">
|
||||
<div id="load-settings-container">
|
||||
<h2>Load Settings From Widget URL</h2>
|
||||
<input id="load-url" type="text">
|
||||
<div style="display: flex;">
|
||||
<button id="cancel-settings" onclick="CloseSettings()">Cancel</button>
|
||||
<span style="width: 20px;"></span>
|
||||
<button id="load-settings" onclick="LoadSettings()">Load Settings</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,237 @@
|
||||
const queryString = window.location.search;
|
||||
const urlParams = new URLSearchParams(queryString);
|
||||
|
||||
const settingsJson = urlParams.get("settingsJson") || "";
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const settingsContent = document.getElementById('settings-content');
|
||||
const saveButton = document.getElementById('save-settings');
|
||||
|
||||
fetch(settingsJson)
|
||||
//fetch('settings.json')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
const groupedSettings = {};
|
||||
|
||||
// Group settings by their 'group' property
|
||||
data.settings.forEach(setting => {
|
||||
if (!groupedSettings[setting.group]) {
|
||||
groupedSettings[setting.group] = [];
|
||||
}
|
||||
groupedSettings[setting.group].push(setting);
|
||||
});
|
||||
|
||||
// Render settings for each group
|
||||
for (const groupName in groupedSettings) {
|
||||
const groupDiv = document.createElement('div');
|
||||
groupDiv.classList.add('setting-group');
|
||||
|
||||
const groupHeader = document.createElement('h2');
|
||||
groupHeader.textContent = groupName;
|
||||
groupDiv.appendChild(groupHeader);
|
||||
|
||||
groupedSettings[groupName].forEach(setting => {
|
||||
const settingItem = document.createElement('div');
|
||||
settingItem.classList.add('setting-item');
|
||||
|
||||
const labelDescriptionDiv = document.createElement('div');
|
||||
|
||||
const label = document.createElement('label');
|
||||
label.textContent = setting.label;
|
||||
labelDescriptionDiv.appendChild(label);
|
||||
|
||||
if (setting.description) {
|
||||
const description = document.createElement('p');
|
||||
description.textContent = setting.description;
|
||||
labelDescriptionDiv.appendChild(description);
|
||||
}
|
||||
|
||||
const settingItemContent = document.createElement('div');
|
||||
settingItemContent.classList.add('setting-item-content');
|
||||
|
||||
let inputElement;
|
||||
switch (setting.type) {
|
||||
case 'text':
|
||||
inputElement = document.createElement('input');
|
||||
inputElement.type = 'text';
|
||||
inputElement.id = setting.id; //Added setting ID
|
||||
inputElement.value = setting.defaultValue;
|
||||
break;
|
||||
case 'checkbox':
|
||||
const labelDiv = document.createElement('label');
|
||||
labelDiv.classList.add('switch');
|
||||
checkBoxElement = document.createElement('input');
|
||||
checkBoxElement.type = 'checkbox';
|
||||
checkBoxElement.id = setting.id;
|
||||
checkBoxElement.checked = setting.defaultValue;
|
||||
labelDiv.appendChild(checkBoxElement);
|
||||
|
||||
const slider = document.createElement('span');
|
||||
slider.classList.add('slider');
|
||||
slider.classList.add('round');
|
||||
labelDiv.appendChild(slider);
|
||||
|
||||
// Add event listener to the switchDiv
|
||||
labelDiv.addEventListener('click', () => {
|
||||
checkBoxElement.checked = !checkBoxElement.checked;
|
||||
});
|
||||
inputElement = labelDiv;
|
||||
break;
|
||||
case 'select':
|
||||
inputElement = document.createElement('select');
|
||||
inputElement.id = setting.id; //Added setting ID
|
||||
setting.options.forEach(option => {
|
||||
const optionElement = document.createElement('option');
|
||||
optionElement.value = option.value;
|
||||
optionElement.textContent = option.label;
|
||||
if (option === setting.defaultValue) {
|
||||
optionElement.selected = true;
|
||||
}
|
||||
inputElement.value = setting.defaultValue;
|
||||
inputElement.appendChild(optionElement);
|
||||
});
|
||||
break;
|
||||
case 'color':
|
||||
inputElement = document.createElement('input');
|
||||
inputElement.type = 'color';
|
||||
inputElement.id = setting.id; //Added setting ID
|
||||
inputElement.value = setting.defaultValue;
|
||||
break;
|
||||
case 'number':
|
||||
inputElement = document.createElement('input');
|
||||
inputElement.type = 'number';
|
||||
inputElement.id = setting.id; //Added setting ID
|
||||
inputElement.value = setting.defaultValue;
|
||||
inputElement.min = setting.min;
|
||||
inputElement.max = setting.max;
|
||||
inputElement.step = setting.step;
|
||||
break;
|
||||
default:
|
||||
inputElement = document.createElement('input');
|
||||
inputElement.type = 'text';
|
||||
inputElement.id = setting.id; //Added setting ID
|
||||
inputElement.value = setting.defaultValue;
|
||||
}
|
||||
|
||||
inputElement.addEventListener('input', function (event) {
|
||||
SendDateToParent(data);
|
||||
});
|
||||
|
||||
settingItemContent.appendChild(inputElement);
|
||||
|
||||
settingItem.appendChild(labelDescriptionDiv);
|
||||
settingItem.appendChild(settingItemContent);
|
||||
groupDiv.appendChild(settingItem);
|
||||
});
|
||||
|
||||
settingsContent.appendChild(groupDiv);
|
||||
}
|
||||
|
||||
// saveButton.addEventListener('click', () => {
|
||||
// SendDateToParent(data);
|
||||
// });
|
||||
SendDateToParent(data);
|
||||
})
|
||||
.catch(error => console.error('Error loading settings:', error));
|
||||
});
|
||||
|
||||
|
||||
// In the iframe's JavaScript:
|
||||
function SendDateToParent(data) {
|
||||
const settings = {};
|
||||
data.settings.forEach(setting => {
|
||||
let inputElement = document.getElementById(setting.id);
|
||||
|
||||
if (setting.type === 'checkbox') {
|
||||
settings[setting.id] = inputElement.checked;
|
||||
} else {
|
||||
settings[setting.id] = inputElement.value;
|
||||
}
|
||||
});
|
||||
|
||||
// Generate parameter string
|
||||
const paramString = Object.entries(settings)
|
||||
.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
|
||||
.join('&');
|
||||
|
||||
console.log('Parameter String:', paramString);
|
||||
|
||||
let widgetURLBox = document.getElementById('widget-url');
|
||||
widgetURLBox.value = GetWidgetURL() + "?" + paramString;
|
||||
window.parent.reloadWidget(paramString);
|
||||
}
|
||||
|
||||
|
||||
let saveButton = document.getElementById('save-settings');
|
||||
let widgetURLBox = document.getElementById('widget-url');
|
||||
let cancelSettingsButton = document.getElementById('cancel-settings');
|
||||
|
||||
saveButton.addEventListener('click', () => {
|
||||
navigator.clipboard.writeText(widgetURLBox.value);
|
||||
|
||||
const defaultBackgroundColor = "#2e2e2e";
|
||||
const defaultTextColor = "white";
|
||||
|
||||
saveButton.innerText = "Copied to clipboard";
|
||||
saveButton.style.backgroundColor = "#00dd63"
|
||||
saveButton.style.color = "#ffffff";
|
||||
|
||||
setTimeout(() => {
|
||||
saveButton.innerText = "Click to copy URL";
|
||||
saveButton.style.backgroundColor = defaultBackgroundColor;
|
||||
saveButton.style.color = defaultTextColor;
|
||||
}, 3000);
|
||||
});
|
||||
|
||||
widgetURLBox.addEventListener('click', () => {
|
||||
let loadSettingsBox = document.getElementById('mommy-milkers');
|
||||
loadSettingsBox.style.visibility = 'visible';
|
||||
loadSettingsBox.style.opacity = 1;
|
||||
});
|
||||
|
||||
function CloseSettings() {
|
||||
let loadSettingsBox = document.getElementById('mommy-milkers');
|
||||
loadSettingsBox.style.visibility = 'hidden';
|
||||
loadSettingsBox.style.opacity = 0;
|
||||
};
|
||||
|
||||
function LoadSettings() {
|
||||
let loadURLBox = document.getElementById('load-url');
|
||||
const url = new URL(loadURLBox.value);
|
||||
|
||||
url.searchParams.forEach((value, key) => {
|
||||
|
||||
const inputElement = document.getElementById(key);
|
||||
if (inputElement != null)
|
||||
{
|
||||
if (inputElement.type == 'checkbox')
|
||||
inputElement.checked = value.toLocaleLowerCase() == 'true';
|
||||
else
|
||||
inputElement.value = value;
|
||||
}
|
||||
});
|
||||
|
||||
loadURLBox.value = '';
|
||||
|
||||
let loadSettingsBox = document.getElementById('mommy-milkers');
|
||||
loadSettingsBox.style.visibility = 'hidden';
|
||||
loadSettingsBox.style.opacity = 0;
|
||||
}
|
||||
|
||||
function GetWidgetURL() {
|
||||
const parsedUrl = new URL(settingsJson);
|
||||
|
||||
let result = parsedUrl.origin; // Base domain (protocol + hostname + port)
|
||||
|
||||
const pathSegments = parsedUrl.pathname.split('/').filter(segment => segment); // Split and remove empty segments
|
||||
|
||||
if (pathSegments.length > 0) {
|
||||
result += '/' + pathSegments[0]; // Add the first path segment
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function OpenMembershipPage() {
|
||||
window.open("https://nutty.gg/supporters/sign_in", '_blank').focus();
|
||||
}
|
||||
@@ -0,0 +1,269 @@
|
||||
* {
|
||||
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica, Arial, sans-serif;
|
||||
font-size: 16px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #181818;
|
||||
}
|
||||
|
||||
body::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
/* Width of the entire scrollbar */
|
||||
}
|
||||
|
||||
body::-webkit-scrollbar-track {
|
||||
background: #2c2c2c;
|
||||
/* Color of the tracking area */
|
||||
}
|
||||
|
||||
body::-webkit-scrollbar-thumb {
|
||||
background-color: #9f9f9f;
|
||||
/* Color of the scroll thumb */
|
||||
border-radius: 4px;
|
||||
/* Roundness of the scroll thumb */
|
||||
border: none;
|
||||
}
|
||||
|
||||
body::-webkit-scrollbar-thumb:hover {
|
||||
background-color: #d1d1d1;
|
||||
/* Color of the scroll thumb on hover */
|
||||
}
|
||||
|
||||
#pinned-header {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
margin: 40px auto;
|
||||
padding: 10px 0;
|
||||
z-index: 100;
|
||||
background: #181818af;
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
#widget-url {
|
||||
font-size: 1em;
|
||||
width: 100%;
|
||||
margin: 10px 0px;
|
||||
padding: 10px 10px;
|
||||
}
|
||||
|
||||
#settings-container {
|
||||
margin: 0px auto;
|
||||
/* border: 1px solid #ddd; */
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
#settings-container h1 {
|
||||
text-align: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.setting-group {
|
||||
margin-bottom: 20px;
|
||||
/* border: 1px solid #ddd; */
|
||||
padding: 15px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 0.9em;
|
||||
font-weight: 500;
|
||||
color: #93cdfd;
|
||||
}
|
||||
|
||||
.setting-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 10px 0;
|
||||
/* border-bottom: 1px solid #eee; */
|
||||
}
|
||||
|
||||
.setting-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.setting-item label {
|
||||
font-weight: 500;
|
||||
margin-bottom: 5px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.setting-item p {
|
||||
font-size: 0.9em;
|
||||
font-weight: 300;
|
||||
/* color: #666; */
|
||||
opacity: 0.6;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.setting-item-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
input[type="text"],
|
||||
select,
|
||||
input[type="number"] {
|
||||
width: 250px;
|
||||
padding: 5px;
|
||||
|
||||
margin: 10px 0px;
|
||||
padding: 10px 10px;
|
||||
background-color: #ffffff05;
|
||||
border-width: 0px;
|
||||
color: white;
|
||||
border-radius: 0.5em;
|
||||
|
||||
-webkit-box-sizing: border-box;
|
||||
-moz-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
#widget-url:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.setting-item-content option {
|
||||
color: black;
|
||||
}
|
||||
|
||||
textarea:focus,
|
||||
input:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.setting-item-content input[type="color"] {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
margin-left: 10px;
|
||||
/* padding: 2px; */
|
||||
/* border: 1px solid #ccc; */
|
||||
/* border-radius: 4px; */
|
||||
|
||||
appearance: none;
|
||||
-moz-appearance: none;
|
||||
-webkit-appearance: none;
|
||||
background: none;
|
||||
border: 0;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* Switch styling */
|
||||
.switch {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 60px;
|
||||
height: 34px;
|
||||
}
|
||||
|
||||
.switch input {
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.slider {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: #ccc;
|
||||
-webkit-transition: .4s;
|
||||
transition: .4s;
|
||||
}
|
||||
|
||||
.slider:before {
|
||||
position: absolute;
|
||||
content: "";
|
||||
height: 26px;
|
||||
width: 26px;
|
||||
left: 4px;
|
||||
bottom: 4px;
|
||||
background-color: white;
|
||||
-webkit-transition: .4s;
|
||||
transition: .4s;
|
||||
}
|
||||
|
||||
input:checked+.slider {
|
||||
background-color: #2196F3;
|
||||
}
|
||||
|
||||
input:focus+.slider {
|
||||
box-shadow: 0 0 1px #2196F3;
|
||||
}
|
||||
|
||||
input:checked+.slider:before {
|
||||
-webkit-transform: translateX(26px);
|
||||
-ms-transform: translateX(26px);
|
||||
transform: translateX(26px);
|
||||
}
|
||||
|
||||
/* Rounded sliders */
|
||||
.slider.round {
|
||||
border-radius: 34px;
|
||||
}
|
||||
|
||||
.slider.round:before {
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
button {
|
||||
font-weight: 500;
|
||||
background-color: #2e2e2e;
|
||||
color: white;
|
||||
opacity: 0.8;
|
||||
margin: 5px 0px;
|
||||
border-width: 0;
|
||||
border-radius: 0.5em;
|
||||
padding: 10px 20px;
|
||||
width: 100%;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
opacity: 1;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#mommy-milkers {
|
||||
visibility: hidden;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: #181818af;
|
||||
backdrop-filter: blur(10px); /* Adjust blur radius as needed */
|
||||
z-index: 9999; /* Ensure it's on top */
|
||||
/* Add any other styles for the overlay content */
|
||||
transition: all 0.2s ease-in-out;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
#load-url {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#load-settings-container {
|
||||
position: fixed;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
width: calc(100% - 75px);
|
||||
border-radius: 0.5em;
|
||||
padding: 1em;
|
||||
box-shadow: 5px 5px 10px rgba(0, 0, 0, 0.5);
|
||||
background: #181818af;
|
||||
/* background: rgba(255, 0, 0, 0.637); */
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
#load-settings {
|
||||
background-color: #2196F3;
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<head>
|
||||
<title>Configure</title>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="shortcut icon" href="#" />
|
||||
<link rel="stylesheet" href="./style.css" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="mainContainer">
|
||||
<div id="connectBox">
|
||||
<img src="logo.png" width="60%">
|
||||
<label>
|
||||
<br><br>
|
||||
<button onclick="RequestToken()">Connect</button>
|
||||
<br><br>
|
||||
<button id="instructionsButton" onclick="OpenInstructions()">Instructions</button>
|
||||
</label>
|
||||
</div>
|
||||
<div id="authorizationBox">
|
||||
<label id="authorizationCode">
|
||||
2305
|
||||
</label>
|
||||
<label id="authorizationComplete">
|
||||
<img src="logo.png" width="60%">
|
||||
<br><br>
|
||||
<span style="font-weight: 500; font-size: 30px;">Authorization complete</span>
|
||||
<br><br>
|
||||
You now owe me money for all of my hard work.
|
||||
</label>
|
||||
<button id="copyURLButton" onclick="CopyToURL()" disabled="true">Awaiting approval</button>
|
||||
<br><br>
|
||||
<button id="donateButton" onclick="OpenDonationPage()" style="display: none;">Donate out of guilt</button>
|
||||
</div>
|
||||
<div id="errorBox">
|
||||
<label style="font-size: 30px; font-weight: 600;">Error <span id="errorCode">69</span></label>
|
||||
<br>
|
||||
<span id="errorMessage">You died lmao. You died lmao. You died lmao. You died lmao. You died lmao. You died lmao.You died lmao. You died lmao.</span>
|
||||
<br><br>
|
||||
|
||||
<button id="closeErrorBox" onclick="CloseErrorBox()">Okay</button>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
<script src="./script.js"></script>
|
||||
|
After Width: | Height: | Size: 61 KiB |
@@ -0,0 +1,110 @@
|
||||
const appId = "nuttys-ytmdesktop-widget";
|
||||
const appName = "nuttys YouTube Music Widget";
|
||||
const appVersion = "1.0.0";
|
||||
const baseURL = "http://nuttylmao.github.io/youtube-music-widget-compact";
|
||||
|
||||
let browserSourceURL = ""
|
||||
|
||||
// Request a four digit authentication code
|
||||
async function RequestCode() {
|
||||
const response = await fetch("http://localhost:9863/api/v1/auth/requestcode", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
"appId": appId,
|
||||
"appName": appName,
|
||||
"appVersion": appVersion
|
||||
}),
|
||||
headers: {
|
||||
"Content-type": "application/json; charset=UTF-8"
|
||||
}
|
||||
})
|
||||
|
||||
const responseData = await response.json();
|
||||
console.debug(responseData);
|
||||
if (responseData.hasOwnProperty("statusCode"))
|
||||
{
|
||||
document.getElementById("errorCode").innerText = responseData.statusCode;
|
||||
document.getElementById("errorMessage").innerText = responseData.message;
|
||||
document.getElementById("errorBox").style.display = 'inline';
|
||||
}
|
||||
else
|
||||
return await responseData;
|
||||
}
|
||||
|
||||
// Wait for the user to accept the code and return them an access token
|
||||
async function RequestToken() {
|
||||
const requestCode = await RequestCode();
|
||||
const authCode = requestCode.code;
|
||||
console.debug(`Auth Code: ${authCode}`);
|
||||
document.getElementById("authorizationCode").innerText = authCode;
|
||||
|
||||
// Show the authorize popup
|
||||
document.getElementById("authorizationBox").style.display = 'inline';
|
||||
|
||||
const response = await fetch("http://localhost:9863/api/v1/auth/request", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
"appId": appId,
|
||||
"code": authCode
|
||||
}),
|
||||
headers: {
|
||||
"Content-type": "application/json; charset=UTF-8"
|
||||
}
|
||||
})
|
||||
|
||||
const responseData = await response.json();
|
||||
if (responseData.hasOwnProperty("statusCode"))
|
||||
{
|
||||
document.getElementById("errorCode").innerText = responseData.statusCode;
|
||||
document.getElementById("errorMessage").innerText = responseData.message;
|
||||
document.getElementById("errorBox").style.display = 'inline';
|
||||
|
||||
// Hide the authorize popup
|
||||
document.getElementById("authorizationBox").style.display = 'none';
|
||||
}
|
||||
else
|
||||
{
|
||||
const token = responseData.token;
|
||||
console.debug(`Token: ${token}`);
|
||||
browserSourceURL = `${baseURL}?token=${token}`;
|
||||
|
||||
// Enable the Copy URL Button
|
||||
document.getElementById("copyURLButton").disabled = false;
|
||||
document.getElementById("copyURLButton").innerText = "Click to copy URL";
|
||||
|
||||
// Show the donation button
|
||||
document.getElementById("authorizationCode").style.display = 'none';
|
||||
|
||||
// Show the donation button and confirmation text
|
||||
document.getElementById("donateButton").style.display = 'block';
|
||||
document.getElementById("authorizationComplete").style.display = 'block';
|
||||
}
|
||||
|
||||
return await responseData;
|
||||
}
|
||||
|
||||
function CopyToURL() {
|
||||
navigator.clipboard.writeText(browserSourceURL);
|
||||
|
||||
document.getElementById("copyURLButton").innerText = "Copied to clipboard";
|
||||
document.getElementById("copyURLButton").style.backgroundColor = "#00dd63"
|
||||
document.getElementById("copyURLButton").style.color = "#ffffff";
|
||||
|
||||
setTimeout(() => {
|
||||
document.getElementById("copyURLButton").innerText = "Click to copy URL";
|
||||
document.getElementById("copyURLButton").style.backgroundColor = "#ffffff";
|
||||
document.getElementById("copyURLButton").style.color = "#181818";
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
function OpenInstructions() {
|
||||
window.open("https://nuttylmao.notion.site/YouTube-Music-Widget-Compact-Edition-1ac19969b23780fb938deaeb00d90800", '_blank').focus();
|
||||
}
|
||||
|
||||
function OpenDonationPage() {
|
||||
window.open("http://nutty.gg/pages/donate", '_blank').focus();
|
||||
}
|
||||
|
||||
function CloseErrorBox() {
|
||||
document.getElementById("errorBox").style.display = 'none';
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
* {
|
||||
--corner-radius: 10px;
|
||||
--padding: 20px;
|
||||
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0px;
|
||||
color: white;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
#mainContainer {
|
||||
position: absolute;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
margin: 0px;
|
||||
background-color: #282828;
|
||||
}
|
||||
|
||||
#errorBox {
|
||||
background-color: #6d0b0b;
|
||||
border-radius: var(--corner-radius);
|
||||
padding: calc(var(--padding)) calc(var(--padding)*2);
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translateX(-50%) translateY(-50%);
|
||||
filter: drop-shadow(0px 0px 4px rgba(0, 0, 0, 1));
|
||||
display: none;
|
||||
}
|
||||
|
||||
#connectBox{
|
||||
background-color: #181818;
|
||||
border-radius: var(--corner-radius);
|
||||
padding: calc(var(--padding)*3) calc(var(--padding)*3);
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translateX(-50%) translateY(-50%);
|
||||
filter: drop-shadow(0px 0px 4px rgba(0, 0, 0, 1));
|
||||
width: 400px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#authorizationBox {
|
||||
background-color: #252525;
|
||||
border-radius: var(--corner-radius);
|
||||
padding: calc(var(--padding)*3) calc(var(--padding)*3);
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translateX(-50%) translateY(-50%);
|
||||
filter: drop-shadow(0px 0px 4px rgba(0, 0, 0, 1));
|
||||
width: 400px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
#authorizationCode {
|
||||
display: block;
|
||||
text-align: center;
|
||||
font-size: 80px;
|
||||
font-weight: 700;
|
||||
padding-left: 30px;
|
||||
padding-bottom: 40px;
|
||||
letter-spacing: 30px;
|
||||
color: #f44336;
|
||||
}
|
||||
|
||||
#authorizationComplete {
|
||||
display: block;
|
||||
text-align: center;
|
||||
padding-bottom: 40px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
button {
|
||||
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica, Arial, sans-serif;
|
||||
font-size: 24px;
|
||||
font-weight: 500;
|
||||
background-color: #f44336;
|
||||
color: white;
|
||||
opacity: 0.8;
|
||||
border-width: 0;
|
||||
border-radius: var(--corner-radius);
|
||||
padding: calc(var(--padding)/2) var(--padding);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
opacity: 1;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#instructionsButton, #donateButton {
|
||||
background-color: #2e2e2e;
|
||||
}
|
||||
|
||||
#copyURLButton {
|
||||
background-color: #ffffff;
|
||||
color: #181818;
|
||||
width: 100%;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
#copyURLButton:disabled {
|
||||
background-color: #636363;
|
||||
opacity: 0.8;
|
||||
cursor: inherit;
|
||||
}
|
||||
|
||||
#closeErrorBox {
|
||||
background-color: #ffffff;
|
||||
color: #181818;
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<head>
|
||||
<title>YouTube Music Widget</title>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="shortcut icon" href="#" />
|
||||
<link rel="stylesheet" href="./style.css" />
|
||||
<script src="https://cdn.socket.io/4.7.5/socket.io.min.js"
|
||||
integrity="sha384-2huaZvOR9iDzHqslqwpR87isEmrfxqyWOF7hr7BY6KG0+hVKLoEXMPUJw3ynWuhO"
|
||||
crossorigin="anonymous"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="statusContainer">Connecting...</div>
|
||||
<div id="mainContainer">
|
||||
<!-- <div id="albumArtBox">
|
||||
<img id="albumArt"
|
||||
src="https://static.wikia.nocookie.net/dreamcatcherwiki/images/d/dc/Dystopia_Lose_Myself_Digital_Cover.jpg"></img>
|
||||
<img id="albumArtBack"
|
||||
src="https://cdns-images.dzcdn.net/images/cover/762f7ef8a0c20e91304a3fae47449ccd/1900x1900-000000-80-0-0.jpg"></img>
|
||||
</div> -->
|
||||
<div id="songInfoBox">
|
||||
<div id="songInfo">
|
||||
<div id="IAmRunningOutOfNamesForTheseBoxes">
|
||||
<div id="songLabel">Can't get you out of my mind</div>
|
||||
<div id="artistLabel">Dreamcatcher</div>
|
||||
<!-- <div id="times">
|
||||
<div id="progressTime">2:29</div>
|
||||
<div id="duration">3:43</div>
|
||||
</div>
|
||||
<div id="progressBg">
|
||||
<div id="progressBar"></div>
|
||||
</div> -->
|
||||
</div>
|
||||
</div>
|
||||
<div id="backgroundArt">
|
||||
<img id="backgroundImage"
|
||||
src="https://static.wikia.nocookie.net/dreamcatcherwiki/images/d/dc/Dystopia_Lose_Myself_Digital_Cover.jpg"></img>
|
||||
<img id="backgroundImageBack"
|
||||
src="https://cdns-images.dzcdn.net/images/cover/762f7ef8a0c20e91304a3fae47449ccd/1900x1900-000000-80-0-0.jpg"></img>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
<script src="./script.js"></script>
|
||||
@@ -0,0 +1,233 @@
|
||||
///////////////
|
||||
// PARAMETRS //
|
||||
///////////////
|
||||
|
||||
const baseURL = "https://nuttylmao.github.io/youtube-music-widget";
|
||||
const queryString = window.location.search;
|
||||
const urlParams = new URLSearchParams(queryString);
|
||||
|
||||
const token = urlParams.get("token") || "";
|
||||
const visibilityDuration = urlParams.get("duration") || 0;
|
||||
// const hideAlbumArt = urlParams.has("hideAlbumArt");
|
||||
|
||||
|
||||
/////////////////
|
||||
// GLOBAL VARS //
|
||||
/////////////////
|
||||
|
||||
let animationSpeed = 0.5;
|
||||
let currentState = 0;
|
||||
|
||||
|
||||
////////////
|
||||
// SOCKET //
|
||||
////////////
|
||||
|
||||
function connectws() {
|
||||
const socket = io("http://localhost:9863/api/v1/realtime", {
|
||||
transports: ['websocket'],
|
||||
auth: {
|
||||
token: token
|
||||
}
|
||||
});
|
||||
|
||||
socket.on("state-update", (state) => {
|
||||
console.debug(state);
|
||||
UpdatePlayer(state);
|
||||
});
|
||||
|
||||
socket.on("playlist-created", (playlist) => {
|
||||
console.debug(playlist);
|
||||
});
|
||||
|
||||
socket.on("playlist-delete", (playlistId) => {
|
||||
console.debug(playlistId);
|
||||
});
|
||||
|
||||
socket.on('connect', function () {
|
||||
SetConnectionStatus(true);
|
||||
});
|
||||
|
||||
socket.on('disconnect', function () {
|
||||
SetConnectionStatus(false);
|
||||
setTimeout(connectws, 5000);
|
||||
});
|
||||
}
|
||||
|
||||
function UpdatePlayer(state) {
|
||||
|
||||
if (state.player.trackState != currentState) {
|
||||
// Set thumbnail
|
||||
const songInfo = state.video;
|
||||
const thumbnail = songInfo.thumbnails[songInfo.thumbnails.length - 1].url;
|
||||
console.debug(thumbnail);
|
||||
// UpdateAlbumArt(document.getElementById("albumArt"), thumbnail);
|
||||
UpdateAlbumArt(document.getElementById("backgroundImage"), thumbnail);
|
||||
UpdateAlbumArt(document.getElementById("backgroundImageBack"), thumbnail);
|
||||
|
||||
// Set song info
|
||||
console.debug(`Artist: ${songInfo.author}`);
|
||||
console.debug(`Title: ${songInfo.title}`);
|
||||
UpdateTextLabel(document.getElementById("artistLabel"), songInfo.author);
|
||||
UpdateTextLabel(document.getElementById("songLabel"), songInfo.title);
|
||||
|
||||
// Set player visibility
|
||||
switch (state.player.trackState) {
|
||||
case -1:
|
||||
console.debug("Player State: Unknown");
|
||||
SetVisibility(false);
|
||||
break;
|
||||
case 0:
|
||||
console.debug("Player State: Paused");
|
||||
SetVisibility(false);
|
||||
break;
|
||||
case 2:
|
||||
console.debug("Player State: Buffering");
|
||||
SetVisibility(false);
|
||||
break;
|
||||
case 1:
|
||||
console.debug("Player State: Playing");
|
||||
setTimeout(() => {
|
||||
SetVisibility(true);
|
||||
}, animationSpeed * 1000);
|
||||
break;
|
||||
}
|
||||
|
||||
if (visibilityDuration > 0) {
|
||||
setTimeout(() => {
|
||||
SetVisibility(false);
|
||||
}, visibilityDuration * 1000);
|
||||
}
|
||||
|
||||
currentState = state.player.trackState;
|
||||
}
|
||||
|
||||
// Set progressbar
|
||||
const songInfo = state.video;
|
||||
const progress = ((state.player.videoProgress / songInfo.durationSeconds) * 100);
|
||||
const progressTime = ConvertSecondsToMinutesSoThatItLooksBetterOnTheOverlay(state.player.videoProgress);
|
||||
const duration = ConvertSecondsToMinutesSoThatItLooksBetterOnTheOverlay(songInfo.durationSeconds - state.player.videoProgress);
|
||||
console.debug(`Progress: ${progressTime}`);
|
||||
console.debug(`Duration: ${duration}`);
|
||||
// document.getElementById("progressBar").style.width = `${progress}%`;
|
||||
// document.getElementById("progressTime").innerHTML = progressTime;
|
||||
// document.getElementById("duration").innerHTML = `-${duration}`;
|
||||
document.getElementById("backgroundImage").style.clipPath = `inset(0 ${100 - progress}% 0 0)`;
|
||||
|
||||
}
|
||||
|
||||
function UpdateTextLabel(div, text) {
|
||||
if (div.innerHTML != text) {
|
||||
div.setAttribute("class", "text-fade");
|
||||
setTimeout(() => {
|
||||
div.innerHTML = text;
|
||||
div.setAttribute("class", ".text-show");
|
||||
}, animationSpeed * 250);
|
||||
}
|
||||
}
|
||||
|
||||
function UpdateAlbumArt(div, imgsrc) {
|
||||
if (div.src != imgsrc) {
|
||||
div.setAttribute("class", "text-fade");
|
||||
setTimeout(() => {
|
||||
div.src = imgsrc;
|
||||
div.setAttribute("class", "text-show");
|
||||
}, animationSpeed * 500);
|
||||
}
|
||||
}
|
||||
|
||||
//////////////////////
|
||||
// HELPER FUNCTIONS //
|
||||
//////////////////////
|
||||
|
||||
function ConvertSecondsToMinutesSoThatItLooksBetterOnTheOverlay(time) {
|
||||
const minutes = Math.floor(time / 60);
|
||||
const seconds = Math.trunc(time - minutes * 60);
|
||||
|
||||
return `${minutes}:${('0' + seconds).slice(-2)}`;
|
||||
}
|
||||
|
||||
function SetVisibility(isVisible) {
|
||||
widgetVisibility = isVisible;
|
||||
|
||||
const mainContainer = document.getElementById("mainContainer");
|
||||
|
||||
if (isVisible) {
|
||||
mainContainer.style.opacity = 1;
|
||||
mainContainer.style.bottom = "50%";
|
||||
}
|
||||
else {
|
||||
mainContainer.style.opacity = 0;
|
||||
mainContainer.style.bottom = "calc(50% - 20px)";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////////////
|
||||
// STREAMER.BOT WEBSOCKET STATUS //
|
||||
///////////////////////////////////
|
||||
|
||||
// This function sets the visibility of the Streamer.bot status label on the overlay
|
||||
function SetConnectionStatus(connected) {
|
||||
let statusContainer = document.getElementById("statusContainer");
|
||||
if (connected) {
|
||||
statusContainer.style.background = "#2FB774";
|
||||
statusContainer.innerText = "Connected!";
|
||||
statusContainer.style.opacity = 1;
|
||||
setTimeout(() => {
|
||||
statusContainer.style.transition = "all 2s ease";
|
||||
statusContainer.style.opacity = 0;
|
||||
}, 10);
|
||||
|
||||
console.log("Connected!");
|
||||
}
|
||||
else {
|
||||
// statusContainer.style.background = "#D12025";
|
||||
// statusContainer.innerText = "Connecting...";
|
||||
// statusContainer.style.opacity = 1;
|
||||
// setTimeout(() => {
|
||||
// statusContainer.style.transition = "all 2s ease";
|
||||
// statusContainer.style.opacity = 0;
|
||||
// }, 10);
|
||||
SetVisibility(false);
|
||||
console.log("Not connected...");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////
|
||||
// RESIZER THING BECAUSE I THINK I KNOW HOW RESPONSIVE DESIGN WORKS EVEN THOUGH I DON'T //
|
||||
//////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
let outer = document.getElementById('mainContainer'),
|
||||
maxWidth = outer.clientWidth+100,
|
||||
maxHeight = outer.clientHeight;
|
||||
|
||||
window.addEventListener("resize", resize);
|
||||
|
||||
resize();
|
||||
function resize() {
|
||||
const scale = window.innerWidth / maxWidth;
|
||||
outer.style.transform = 'translate(-50%, 50%) scale(' + scale + ')';
|
||||
}
|
||||
|
||||
|
||||
|
||||
/////////////////////////////////////////////////////////////////////
|
||||
// IF THE USER PUT IN THE HIDEALBUMART PARAMATER, THEN YOU SHOULD //
|
||||
// HIDE THE ALBUM ART, BECAUSE THAT'S WHAT IT'S SUPPOSED TO DO //
|
||||
/////////////////////////////////////////////////////////////////////
|
||||
|
||||
// if (hideAlbumArt) {
|
||||
// document.getElementById("albumArtBox").style.display = "none";
|
||||
// document.getElementById("songInfoBox").style.width = "calc(100% - 20px)";
|
||||
// }
|
||||
|
||||
if (token == "") {
|
||||
console.log("No token detected...");
|
||||
window.open(`${baseURL}/configure`);
|
||||
}
|
||||
else
|
||||
connectws();
|
||||
@@ -0,0 +1,183 @@
|
||||
* {
|
||||
--corner-radius: 25px;
|
||||
--album-art-size: 50px;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
#statusContainer {
|
||||
font-weight: 500;
|
||||
font-size: 30px;
|
||||
text-align: center;
|
||||
background-color: #D12025;
|
||||
color: white;
|
||||
padding: 10px;
|
||||
border-radius: 10px;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
#mainContainer {
|
||||
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica, Arial, sans-serif;
|
||||
display: flex;
|
||||
position: absolute;
|
||||
height: var(--album-art-size);
|
||||
margin: 20px;
|
||||
filter: drop-shadow(15px 15px 7px rgba(0, 0, 0, 1));
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
bottom: calc(50% - 20px);
|
||||
left: 50%;
|
||||
opacity: 0;
|
||||
transition: all 0.5s ease;
|
||||
}
|
||||
|
||||
/* #albumArtBox {
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
position: relative;
|
||||
border-radius: var(--corner-radius);
|
||||
overflow: hidden;
|
||||
margin: 0px 8px 0px 0px;
|
||||
object-fit: cover;
|
||||
width: var(--album-art-size);
|
||||
}
|
||||
|
||||
#albumArtBox img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
} */
|
||||
|
||||
#songInfoBox {
|
||||
position: relative;
|
||||
color: white;
|
||||
/* width: calc(100% - 125px); */
|
||||
width: calc(100%);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 0 1 auto;
|
||||
justify-content: center;
|
||||
z-index: 1;
|
||||
text-shadow: 2px 2px 2px rgba(0, 0, 0, 0.5);
|
||||
overflow: hidden;
|
||||
z-index: 4;
|
||||
}
|
||||
|
||||
#songInfo {
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
opacity: 0.9;
|
||||
position: relative;
|
||||
border-radius: var(--corner-radius);
|
||||
padding: 0px 20px;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#IAmRunningOutOfNamesForTheseBoxes {
|
||||
position: absolute;
|
||||
width: calc(100% - 40px);
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
-ms-transform: translateY(-50%);
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
#backgroundArt {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
border-radius: var(--corner-radius);
|
||||
overflow: hidden;
|
||||
z-index: -1;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
#backgroundImage {
|
||||
filter: blur(20px);
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%) translateY(-50%);
|
||||
}
|
||||
|
||||
#backgroundImageBack {
|
||||
filter: blur(20px) grayscale(75%);
|
||||
opacity: 0.5;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%) translateY(-50%);
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
#artistLabel {
|
||||
font-size: 18px;
|
||||
font-weight: 400;
|
||||
line-height: 1;
|
||||
white-space: nowrap;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#songLabel {
|
||||
font-weight: bold;
|
||||
font-size: 20px;
|
||||
width: calc(100%);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* #progressBg {
|
||||
display: none;
|
||||
margin-top: 15px;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
border-radius: 5px;
|
||||
background-color: #1F1F1F;
|
||||
} */
|
||||
|
||||
/* #progressBar {
|
||||
border-radius: 5px;
|
||||
height: 5px;
|
||||
width: 20%;
|
||||
background-color: #ffffff;
|
||||
margin: 10px 0px;
|
||||
transition: all 1s ease;
|
||||
} */
|
||||
|
||||
/* #times {
|
||||
display: none;
|
||||
position: relative;
|
||||
height: 16px;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
line-height: 2.2;
|
||||
} */
|
||||
|
||||
/* #progressTime {
|
||||
position: absolute;
|
||||
} */
|
||||
|
||||
/* #duration {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
text-align: right;
|
||||
} */
|
||||
|
||||
.text-show {
|
||||
opacity: 1;
|
||||
transition: all 0.25s ease;
|
||||
}
|
||||
|
||||
.text-fade {
|
||||
opacity: 0;
|
||||
transition: all 0.25s ease;
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<head>
|
||||
<title>Configure</title>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="shortcut icon" href="#" />
|
||||
<link rel="stylesheet" href="./style.css" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="mainContainer">
|
||||
<div id="connectBox">
|
||||
<img src="logo.png" width="60%">
|
||||
<label>
|
||||
<br><br>
|
||||
<button onclick="RequestToken()">Connect</button>
|
||||
<br><br>
|
||||
<button id="instructionsButton" onclick="OpenInstructions()">Instructions</button>
|
||||
</label>
|
||||
</div>
|
||||
<div id="authorizationBox">
|
||||
<label id="authorizationCode">
|
||||
2305
|
||||
</label>
|
||||
<label id="authorizationComplete">
|
||||
<img src="logo.png" width="60%">
|
||||
<br><br>
|
||||
<span style="font-weight: 500; font-size: 30px;">Authorization complete</span>
|
||||
<br><br>
|
||||
You now owe me money for all of my hard work.
|
||||
</label>
|
||||
<button id="copyURLButton" onclick="CopyToURL()" disabled="true">Awaiting approval</button>
|
||||
<br><br>
|
||||
<button id="donateButton" onclick="OpenDonationPage()" style="display: none;">Donate out of guilt</button>
|
||||
</div>
|
||||
<div id="errorBox">
|
||||
<label style="font-size: 30px; font-weight: 600;">Error <span id="errorCode">69</span></label>
|
||||
<br>
|
||||
<span id="errorMessage">You died lmao. You died lmao. You died lmao. You died lmao. You died lmao. You died lmao.You died lmao. You died lmao.</span>
|
||||
<br><br>
|
||||
|
||||
<button id="closeErrorBox" onclick="CloseErrorBox()">Okay</button>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
<script src="./script.js"></script>
|
||||
|
After Width: | Height: | Size: 61 KiB |
@@ -0,0 +1,110 @@
|
||||
const appId = "nuttys-ytmdesktop-widget";
|
||||
const appName = "nuttys YouTube Music Widget";
|
||||
const appVersion = "1.0.0";
|
||||
const baseURL = "http://nuttylmao.github.io/youtube-music-widget";
|
||||
|
||||
let browserSourceURL = ""
|
||||
|
||||
// Request a four digit authentication code
|
||||
async function RequestCode() {
|
||||
const response = await fetch("http://localhost:9863/api/v1/auth/requestcode", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
"appId": appId,
|
||||
"appName": appName,
|
||||
"appVersion": appVersion
|
||||
}),
|
||||
headers: {
|
||||
"Content-type": "application/json; charset=UTF-8"
|
||||
}
|
||||
})
|
||||
|
||||
const responseData = await response.json();
|
||||
console.debug(responseData);
|
||||
if (responseData.hasOwnProperty("statusCode"))
|
||||
{
|
||||
document.getElementById("errorCode").innerText = responseData.statusCode;
|
||||
document.getElementById("errorMessage").innerText = responseData.message;
|
||||
document.getElementById("errorBox").style.display = 'inline';
|
||||
}
|
||||
else
|
||||
return await responseData;
|
||||
}
|
||||
|
||||
// Wait for the user to accept the code and return them an access token
|
||||
async function RequestToken() {
|
||||
const requestCode = await RequestCode();
|
||||
const authCode = requestCode.code;
|
||||
console.debug(`Auth Code: ${authCode}`);
|
||||
document.getElementById("authorizationCode").innerText = authCode;
|
||||
|
||||
// Show the authorize popup
|
||||
document.getElementById("authorizationBox").style.display = 'inline';
|
||||
|
||||
const response = await fetch("http://localhost:9863/api/v1/auth/request", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
"appId": appId,
|
||||
"code": authCode
|
||||
}),
|
||||
headers: {
|
||||
"Content-type": "application/json; charset=UTF-8"
|
||||
}
|
||||
})
|
||||
|
||||
const responseData = await response.json();
|
||||
if (responseData.hasOwnProperty("statusCode"))
|
||||
{
|
||||
document.getElementById("errorCode").innerText = responseData.statusCode;
|
||||
document.getElementById("errorMessage").innerText = responseData.message;
|
||||
document.getElementById("errorBox").style.display = 'inline';
|
||||
|
||||
// Hide the authorize popup
|
||||
document.getElementById("authorizationBox").style.display = 'none';
|
||||
}
|
||||
else
|
||||
{
|
||||
const token = responseData.token;
|
||||
console.debug(`Token: ${token}`);
|
||||
browserSourceURL = `${baseURL}?token=${token}`;
|
||||
|
||||
// Enable the Copy URL Button
|
||||
document.getElementById("copyURLButton").disabled = false;
|
||||
document.getElementById("copyURLButton").innerText = "Click to copy URL";
|
||||
|
||||
// Show the donation button
|
||||
document.getElementById("authorizationCode").style.display = 'none';
|
||||
|
||||
// Show the donation button and confirmation text
|
||||
document.getElementById("donateButton").style.display = 'block';
|
||||
document.getElementById("authorizationComplete").style.display = 'block';
|
||||
}
|
||||
|
||||
return await responseData;
|
||||
}
|
||||
|
||||
function CopyToURL() {
|
||||
navigator.clipboard.writeText(browserSourceURL);
|
||||
|
||||
document.getElementById("copyURLButton").innerText = "Copied to clipboard";
|
||||
document.getElementById("copyURLButton").style.backgroundColor = "#00dd63"
|
||||
document.getElementById("copyURLButton").style.color = "#ffffff";
|
||||
|
||||
setTimeout(() => {
|
||||
document.getElementById("copyURLButton").innerText = "Click to copy URL";
|
||||
document.getElementById("copyURLButton").style.backgroundColor = "#ffffff";
|
||||
document.getElementById("copyURLButton").style.color = "#181818";
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
function OpenInstructions() {
|
||||
window.open("https://www.notion.so/nuttylmao/YouTube-Music-Widget-18d19969b23780e2bb56d25eed4d154e", '_blank').focus();
|
||||
}
|
||||
|
||||
function OpenDonationPage() {
|
||||
window.open("http://nutty.gg/pages/donate", '_blank').focus();
|
||||
}
|
||||
|
||||
function CloseErrorBox() {
|
||||
document.getElementById("errorBox").style.display = 'none';
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
* {
|
||||
--corner-radius: 10px;
|
||||
--padding: 20px;
|
||||
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0px;
|
||||
color: white;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
#mainContainer {
|
||||
position: absolute;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
margin: 0px;
|
||||
background-color: #282828;
|
||||
}
|
||||
|
||||
#errorBox {
|
||||
background-color: #6d0b0b;
|
||||
border-radius: var(--corner-radius);
|
||||
padding: calc(var(--padding)) calc(var(--padding)*2);
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translateX(-50%) translateY(-50%);
|
||||
filter: drop-shadow(0px 0px 4px rgba(0, 0, 0, 1));
|
||||
display: none;
|
||||
}
|
||||
|
||||
#connectBox{
|
||||
background-color: #181818;
|
||||
border-radius: var(--corner-radius);
|
||||
padding: calc(var(--padding)*3) calc(var(--padding)*3);
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translateX(-50%) translateY(-50%);
|
||||
filter: drop-shadow(0px 0px 4px rgba(0, 0, 0, 1));
|
||||
width: 400px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#authorizationBox {
|
||||
background-color: #252525;
|
||||
border-radius: var(--corner-radius);
|
||||
padding: calc(var(--padding)*3) calc(var(--padding)*3);
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translateX(-50%) translateY(-50%);
|
||||
filter: drop-shadow(0px 0px 4px rgba(0, 0, 0, 1));
|
||||
width: 400px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
#authorizationCode {
|
||||
display: block;
|
||||
text-align: center;
|
||||
font-size: 80px;
|
||||
font-weight: 700;
|
||||
padding-left: 30px;
|
||||
padding-bottom: 40px;
|
||||
letter-spacing: 30px;
|
||||
color: #f44336;
|
||||
}
|
||||
|
||||
#authorizationComplete {
|
||||
display: block;
|
||||
text-align: center;
|
||||
padding-bottom: 40px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
button {
|
||||
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica, Arial, sans-serif;
|
||||
font-size: 24px;
|
||||
font-weight: 500;
|
||||
background-color: #f44336;
|
||||
color: white;
|
||||
opacity: 0.8;
|
||||
border-width: 0;
|
||||
border-radius: var(--corner-radius);
|
||||
padding: calc(var(--padding)/2) var(--padding);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
opacity: 1;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#instructionsButton, #donateButton {
|
||||
background-color: #2e2e2e;
|
||||
}
|
||||
|
||||
#copyURLButton {
|
||||
background-color: #ffffff;
|
||||
color: #181818;
|
||||
width: 100%;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
#copyURLButton:disabled {
|
||||
background-color: #636363;
|
||||
opacity: 0.8;
|
||||
cursor: inherit;
|
||||
}
|
||||
|
||||
#closeErrorBox {
|
||||
background-color: #ffffff;
|
||||
color: #181818;
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<head>
|
||||
<title>YouTube Music Widget</title>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="shortcut icon" href="#" />
|
||||
<link rel="stylesheet" href="./style.css" />
|
||||
<script src="https://cdn.socket.io/4.7.5/socket.io.min.js"
|
||||
integrity="sha384-2huaZvOR9iDzHqslqwpR87isEmrfxqyWOF7hr7BY6KG0+hVKLoEXMPUJw3ynWuhO"
|
||||
crossorigin="anonymous"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="statusContainer">Connecting...</div>
|
||||
<div id="mainContainer">
|
||||
<div id="albumArtBox">
|
||||
<img id="albumArt"
|
||||
src="https://static.wikia.nocookie.net/dreamcatcherwiki/images/d/dc/Dystopia_Lose_Myself_Digital_Cover.jpg"></img>
|
||||
<img id="albumArtBack"
|
||||
src="https://cdns-images.dzcdn.net/images/cover/762f7ef8a0c20e91304a3fae47449ccd/1900x1900-000000-80-0-0.jpg"></img>
|
||||
</div>
|
||||
<div id="songInfoBox">
|
||||
<div id="songInfo">
|
||||
<div id="IAmRunningOutOfNamesForTheseBoxes">
|
||||
<div id="songLabel">Can't get you out of my mind</div>
|
||||
<div id="artistLabel">Dreamcatcher</div>
|
||||
<div id="times">
|
||||
<div id="progressTime">2:29</div>
|
||||
<div id="duration">3:43</div>
|
||||
</div>
|
||||
<div id="progressBg">
|
||||
<div id="progressBar"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="backgroundArt">
|
||||
<img id="backgroundImage"
|
||||
src="https://static.wikia.nocookie.net/dreamcatcherwiki/images/d/dc/Dystopia_Lose_Myself_Digital_Cover.jpg"></img>
|
||||
<img id="backgroundImageBack"
|
||||
src="https://cdns-images.dzcdn.net/images/cover/762f7ef8a0c20e91304a3fae47449ccd/1900x1900-000000-80-0-0.jpg"></img>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
<script src="./script.js"></script>
|
||||
@@ -0,0 +1,231 @@
|
||||
///////////////
|
||||
// PARAMETRS //
|
||||
///////////////
|
||||
|
||||
const baseURL = "https://nuttylmao.github.io/youtube-music-widget";
|
||||
const queryString = window.location.search;
|
||||
const urlParams = new URLSearchParams(queryString);
|
||||
|
||||
const token = urlParams.get("token") || "";
|
||||
const visibilityDuration = urlParams.get("duration") || 0;
|
||||
const hideAlbumArt = urlParams.has("hideAlbumArt");
|
||||
|
||||
|
||||
/////////////////
|
||||
// GLOBAL VARS //
|
||||
/////////////////
|
||||
|
||||
let animationSpeed = 0.5;
|
||||
let currentState = 0;
|
||||
|
||||
|
||||
////////////
|
||||
// SOCKET //
|
||||
////////////
|
||||
|
||||
function connectws() {
|
||||
const socket = io("http://localhost:9863/api/v1/realtime", {
|
||||
transports: ['websocket'],
|
||||
auth: {
|
||||
token: token
|
||||
}
|
||||
});
|
||||
|
||||
socket.on("state-update", (state) => {
|
||||
console.debug(state);
|
||||
UpdatePlayer(state);
|
||||
});
|
||||
|
||||
socket.on("playlist-created", (playlist) => {
|
||||
console.debug(playlist);
|
||||
});
|
||||
|
||||
socket.on("playlist-delete", (playlistId) => {
|
||||
console.debug(playlistId);
|
||||
});
|
||||
|
||||
socket.on('connect', function () {
|
||||
SetConnectionStatus(true);
|
||||
});
|
||||
|
||||
socket.on('disconnect', function () {
|
||||
SetConnectionStatus(false);
|
||||
setTimeout(connectws, 5000);
|
||||
});
|
||||
}
|
||||
|
||||
function UpdatePlayer(state) {
|
||||
|
||||
if (state.player.trackState != currentState) {
|
||||
// Set thumbnail
|
||||
const songInfo = state.video;
|
||||
const thumbnail = songInfo.thumbnails[songInfo.thumbnails.length - 1].url;
|
||||
console.debug(thumbnail);
|
||||
UpdateAlbumArt(document.getElementById("albumArt"), thumbnail);
|
||||
UpdateAlbumArt(document.getElementById("backgroundImage"), thumbnail);
|
||||
|
||||
// Set song info
|
||||
console.debug(`Artist: ${songInfo.author}`);
|
||||
console.debug(`Title: ${songInfo.title}`);
|
||||
UpdateTextLabel(document.getElementById("artistLabel"), songInfo.author);
|
||||
UpdateTextLabel(document.getElementById("songLabel"), songInfo.title);
|
||||
|
||||
// Set player visibility
|
||||
switch (state.player.trackState) {
|
||||
case -1:
|
||||
console.debug("Player State: Unknown");
|
||||
SetVisibility(false);
|
||||
break;
|
||||
case 0:
|
||||
console.debug("Player State: Paused");
|
||||
SetVisibility(false);
|
||||
break;
|
||||
case 2:
|
||||
console.debug("Player State: Buffering");
|
||||
SetVisibility(false);
|
||||
break;
|
||||
case 1:
|
||||
console.debug("Player State: Playing");
|
||||
setTimeout(() => {
|
||||
SetVisibility(true);
|
||||
}, animationSpeed * 1000);
|
||||
break;
|
||||
}
|
||||
|
||||
if (visibilityDuration > 0) {
|
||||
setTimeout(() => {
|
||||
SetVisibility(false);
|
||||
}, visibilityDuration * 1000);
|
||||
}
|
||||
|
||||
currentState = state.player.trackState;
|
||||
}
|
||||
|
||||
// Set progressbar
|
||||
const songInfo = state.video;
|
||||
const progress = ((state.player.videoProgress / songInfo.durationSeconds) * 100);
|
||||
const progressTime = ConvertSecondsToMinutesSoThatItLooksBetterOnTheOverlay(state.player.videoProgress);
|
||||
const duration = ConvertSecondsToMinutesSoThatItLooksBetterOnTheOverlay(songInfo.durationSeconds - state.player.videoProgress);
|
||||
console.debug(`Progress: ${progressTime}`);
|
||||
console.debug(`Duration: ${duration}`);
|
||||
document.getElementById("progressBar").style.width = `${progress}%`;
|
||||
document.getElementById("progressTime").innerHTML = progressTime;
|
||||
document.getElementById("duration").innerHTML = `-${duration}`;
|
||||
|
||||
}
|
||||
|
||||
function UpdateTextLabel(div, text) {
|
||||
if (div.innerHTML != text) {
|
||||
div.setAttribute("class", "text-fade");
|
||||
setTimeout(() => {
|
||||
div.innerHTML = text;
|
||||
div.setAttribute("class", ".text-show");
|
||||
}, animationSpeed * 250);
|
||||
}
|
||||
}
|
||||
|
||||
function UpdateAlbumArt(div, imgsrc) {
|
||||
if (div.src != imgsrc) {
|
||||
div.setAttribute("class", "text-fade");
|
||||
setTimeout(() => {
|
||||
div.src = imgsrc;
|
||||
div.setAttribute("class", "text-show");
|
||||
}, animationSpeed * 500);
|
||||
}
|
||||
}
|
||||
|
||||
//////////////////////
|
||||
// HELPER FUNCTIONS //
|
||||
//////////////////////
|
||||
|
||||
function ConvertSecondsToMinutesSoThatItLooksBetterOnTheOverlay(time) {
|
||||
const minutes = Math.floor(time / 60);
|
||||
const seconds = Math.trunc(time - minutes * 60);
|
||||
|
||||
return `${minutes}:${('0' + seconds).slice(-2)}`;
|
||||
}
|
||||
|
||||
function SetVisibility(isVisible) {
|
||||
widgetVisibility = isVisible;
|
||||
|
||||
const mainContainer = document.getElementById("mainContainer");
|
||||
|
||||
if (isVisible) {
|
||||
mainContainer.style.opacity = 1;
|
||||
mainContainer.style.bottom = "50%";
|
||||
}
|
||||
else {
|
||||
mainContainer.style.opacity = 0;
|
||||
mainContainer.style.bottom = "calc(50% - 20px)";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////////////
|
||||
// STREAMER.BOT WEBSOCKET STATUS //
|
||||
///////////////////////////////////
|
||||
|
||||
// This function sets the visibility of the Streamer.bot status label on the overlay
|
||||
function SetConnectionStatus(connected) {
|
||||
let statusContainer = document.getElementById("statusContainer");
|
||||
if (connected) {
|
||||
statusContainer.style.background = "#2FB774";
|
||||
statusContainer.innerText = "Connected!";
|
||||
statusContainer.style.opacity = 1;
|
||||
setTimeout(() => {
|
||||
statusContainer.style.transition = "all 2s ease";
|
||||
statusContainer.style.opacity = 0;
|
||||
}, 10);
|
||||
|
||||
console.log("Connected!");
|
||||
}
|
||||
else {
|
||||
// statusContainer.style.background = "#D12025";
|
||||
// statusContainer.innerText = "Connecting...";
|
||||
// statusContainer.style.opacity = 1;
|
||||
// setTimeout(() => {
|
||||
// statusContainer.style.transition = "all 2s ease";
|
||||
// statusContainer.style.opacity = 0;
|
||||
// }, 10);
|
||||
SetVisibility(false);
|
||||
console.log("Not connected...");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////
|
||||
// RESIZER THING BECAUSE I THINK I KNOW HOW RESPONSIVE DESIGN WORKS EVEN THOUGH I DON'T //
|
||||
//////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
let outer = document.getElementById('mainContainer'),
|
||||
maxWidth = outer.clientWidth+50,
|
||||
maxHeight = outer.clientHeight;
|
||||
|
||||
window.addEventListener("resize", resize);
|
||||
|
||||
resize();
|
||||
function resize() {
|
||||
const scale = window.innerWidth / maxWidth;
|
||||
outer.style.transform = 'translate(-50%, 50%) scale(' + scale + ')';
|
||||
}
|
||||
|
||||
|
||||
|
||||
/////////////////////////////////////////////////////////////////////
|
||||
// IF THE USER PUT IN THE HIDEALBUMART PARAMATER, THEN YOU SHOULD //
|
||||
// HIDE THE ALBUM ART, BECAUSE THAT'S WHAT IT'S SUPPOSED TO DO //
|
||||
/////////////////////////////////////////////////////////////////////
|
||||
|
||||
if (hideAlbumArt) {
|
||||
document.getElementById("albumArtBox").style.display = "none";
|
||||
document.getElementById("songInfoBox").style.width = "calc(100% - 20px)";
|
||||
}
|
||||
|
||||
if (token == "") {
|
||||
console.log("No token detected...");
|
||||
window.open(`${baseURL}/configure`);
|
||||
}
|
||||
else
|
||||
connectws();
|
||||
@@ -0,0 +1,177 @@
|
||||
* {
|
||||
--corner-radius: 10px;
|
||||
--album-art-size: 100px;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
#statusContainer {
|
||||
font-weight: 500;
|
||||
font-size: 30px;
|
||||
text-align: center;
|
||||
background-color: #D12025;
|
||||
color: white;
|
||||
padding: 10px;
|
||||
border-radius: 10px;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
#mainContainer {
|
||||
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica, Arial, sans-serif;
|
||||
display: flex;
|
||||
position: absolute;
|
||||
height: var(--album-art-size);
|
||||
margin: 20px;
|
||||
filter: drop-shadow(15px 15px 7px rgba(0, 0, 0, 1));
|
||||
width: 100%;
|
||||
max-width: 500px;
|
||||
bottom: calc(50% - 20px);
|
||||
left: 50%;
|
||||
opacity: 0;
|
||||
transition: all 0.5s ease;
|
||||
}
|
||||
|
||||
#albumArtBox {
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
position: relative;
|
||||
border-radius: var(--corner-radius);
|
||||
overflow: hidden;
|
||||
margin: 0px 8px 0px 0px;
|
||||
object-fit: cover;
|
||||
width: var(--album-art-size);
|
||||
}
|
||||
|
||||
#albumArtBox img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
#songInfoBox {
|
||||
position: relative;
|
||||
color: white;
|
||||
width: calc(100% - 125px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 0 1 auto;
|
||||
justify-content: center;
|
||||
z-index: 1;
|
||||
text-shadow: 2px 2px 2px rgba(0, 0, 0, 0.5);
|
||||
overflow: hidden;
|
||||
z-index: 4;
|
||||
}
|
||||
|
||||
#songInfo {
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
opacity: 0.9;
|
||||
position: relative;
|
||||
border-radius: var(--corner-radius);
|
||||
padding: 0px 20px;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#IAmRunningOutOfNamesForTheseBoxes {
|
||||
position: absolute;
|
||||
width: calc(100% - 40px);
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
-ms-transform: translateY(-50%);
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
#backgroundArt {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
border-radius: var(--corner-radius);
|
||||
overflow: hidden;
|
||||
z-index: -1;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
#backgroundImage {
|
||||
filter: blur(20px);
|
||||
position: absolute;
|
||||
width: 140%;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%) translateY(-50%);
|
||||
}
|
||||
|
||||
#backgroundImageBack {
|
||||
filter: blur(20px);
|
||||
position: absolute;
|
||||
width: 140%;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%) translateY(-50%);
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
#artistLabel {
|
||||
font-size: 16px;
|
||||
font-weight: 400;
|
||||
line-height: 1;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
#songLabel {
|
||||
font-weight: bold;
|
||||
font-size: 20px;
|
||||
width: calc(100%);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
#progressBg {
|
||||
margin-top: 15px;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
border-radius: 5px;
|
||||
background-color: #1F1F1F;
|
||||
}
|
||||
|
||||
#progressBar {
|
||||
border-radius: 5px;
|
||||
height: 5px;
|
||||
width: 20%;
|
||||
background-color: #ffffff;
|
||||
margin: 10px 0px;
|
||||
transition: all 1s ease;
|
||||
}
|
||||
|
||||
#times {
|
||||
position: relative;
|
||||
height: 16px;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
line-height: 2.2;
|
||||
}
|
||||
|
||||
#progressTime {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
#duration {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.text-show {
|
||||
opacity: 1;
|
||||
transition: all 0.25s ease;
|
||||
}
|
||||
|
||||
.text-fade {
|
||||
opacity: 0;
|
||||
transition: all 0.25s ease;
|
||||
}
|
||||