Add files via upload

This commit is contained in:
nuttylmao
2025-03-23 22:04:07 +11:00
committed by GitHub
parent f133722899
commit 6c53d973f7
97 changed files with 15625 additions and 0 deletions
+47
View File
@@ -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>
+230
View File
@@ -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();
+181
View File
@@ -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;
}
+47
View File
@@ -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>
+229
View File
@@ -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();
+177
View File
@@ -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;
}
+8
View File
@@ -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>
File diff suppressed because it is too large Load Diff
+19
View File
@@ -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();
+19
View File
@@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

+38
View File
@@ -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>
+881
View File
@@ -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);
+64
View File
@@ -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>
+29
View File
@@ -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;
}
+200
View File
@@ -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"
}
]
}
+303
View File
@@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

+70
View File
@@ -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>
File diff suppressed because it is too large Load Diff
+64
View File
@@ -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>
+29
View File
@@ -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;
}
+271
View File
@@ -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"
}
]
}
+245
View File
@@ -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;
}
}
+7
View File
@@ -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

Binary file not shown.
+81
View File
@@ -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>
+5
View File
@@ -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
+444
View File
@@ -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();
+201
View File
@@ -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);
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 254 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 604 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 203 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

+80
View File
@@ -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>
+492
View File
@@ -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);
}
+257
View File
@@ -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;
}
+8
View File
@@ -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>
+36
View File
@@ -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}`;
}
+16
View File
@@ -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;
}
+20
View File
@@ -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>
+38
View File
@@ -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);
}
+25
View File
@@ -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>
Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

+131
View File
@@ -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();
}
+104
View File
@@ -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;
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

+46
View File
@@ -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>
+270
View File
@@ -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
+191
View File
@@ -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;
}
+49
View File
@@ -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>
Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

+131
View File
@@ -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();
}
+104
View File
@@ -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;
}
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

+46
View File
@@ -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>
+269
View File
@@ -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
+192
View File
@@ -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;
}
+7
View File
@@ -0,0 +1,7 @@
<link rel="stylesheet" href="./style.css" />
<div id="mainContainer">
<label id="uptimeLabel"></label>
</div>
<script src="./script.js"></script>
+40
View File
@@ -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
}
}
+16
View File
@@ -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;
}
+40
View File
@@ -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>
+237
View File
@@ -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();
}
+269
View File
@@ -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>
Binary file not shown.

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;
}
+46
View File
@@ -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>
+233
View File
@@ -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();
+183
View File
@@ -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;
}
+47
View File
@@ -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>
Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

+110
View File
@@ -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';
}
+115
View File
@@ -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;
}
+46
View File
@@ -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>
+231
View File
@@ -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();
+177
View File
@@ -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;
}