Revert "Merge Beta to Main"
@@ -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 |
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 75 KiB |
|
Before Width: | Height: | Size: 8.4 KiB |
@@ -1,228 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<link rel="stylesheet" href="../../../.common/styles/global.css" />
|
||||
<link rel="stylesheet" href="./style.css" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<!-- Update All Button -->
|
||||
<button onclick="OpenUpdateAllDialog()">
|
||||
<img src="https://api.iconify.design/material-symbols:edit-square-outline.svg?color=%23ffffff"> Update All
|
||||
</button>
|
||||
|
||||
<div style="display: flex; flex-direction: column; gap: 0.5em;">
|
||||
<!-- List of all broadcasts -->
|
||||
<div id="broadcast-list">
|
||||
</div>
|
||||
|
||||
<!-- YouTube warning -->
|
||||
<div id="youtube-warning" class="broadcast">
|
||||
<img class="platform-icon" id="platform-icon" src="icons/platforms/youtube.png" />
|
||||
|
||||
<div class="broadcast-info">
|
||||
<label class="setting-attribute">
|
||||
<i>YouTube stream info can only be updated while live</i>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div id="broadcast-buttons">
|
||||
<button id="broadcast-dashboard-button" class="icon-button" title="Dashboard"><img class="button-icon"
|
||||
src="https://api.iconify.design/gridicons:popout.svg?color=%23ffffff"></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Blur Layer -->
|
||||
<div id="blur-layer" class="blur"></div>
|
||||
|
||||
<!-- Update All Popup -->
|
||||
<div id="update-all-dialog" class="dialog">
|
||||
<button class="dialog-nav-button" onclick="CloseUpdateAllDialog()">×</button>
|
||||
|
||||
<label class="title" style="text-align: center;">Stream Info</label>
|
||||
|
||||
<div class="field">
|
||||
<div style="display: flex; flex-direction: row; align-items: end; gap: 1em;">
|
||||
<label>Title</label>
|
||||
<div id="all-title-char-limit" class="char-limit">
|
||||
<span id="all-title-char-count"></span>/<span id="all-title-char-max"></span>
|
||||
</div>
|
||||
</div>
|
||||
<input type="text" id="all-title-input" placeholder="Leave empty to keep current title">
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label>Category</label>
|
||||
<input type="text" id="all-category-input" placeholder="Leave empty to keep current category">
|
||||
</div>
|
||||
|
||||
<button id="all-submit-button" onclick="UpdateAllSubmit()">Update All</button>
|
||||
</div>
|
||||
|
||||
<!-- Update Twitch Popup -->
|
||||
<div id="update-twitch-dialog" class="dialog">
|
||||
<button class="dialog-nav-button" onclick="CloseUpdateTwitchDialog()">×</button>
|
||||
|
||||
<div style="display: flex; flex-direction: row; align-items: center; justify-content: center; gap: 1em;">
|
||||
<img src="icons/platforms/twitch.png" class="platform-icon">
|
||||
<label class="title" style="text-align: center;">Stream Info</label>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<div style="display: flex; flex-direction: row; align-items: end; gap: 1em;">
|
||||
<label>Title</label>
|
||||
<div id="twitch-title-char-limit" class="char-limit">
|
||||
<span id="twitch-title-char-count"></span>/<span id="twitch-title-char-max"></span>
|
||||
</div>
|
||||
</div>
|
||||
<input type="text" id="twitch-title-input" placeholder="Leave empty to keep current title">
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label>Category</label>
|
||||
<input type="text" id="twitch-category-input" placeholder="Leave empty to keep current category">
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<div style="display: flex; flex-direction: row; align-items: end; gap: 1em;">
|
||||
<label>Tags</label>
|
||||
<div id="twitch-tags-char-limit" class="char-limit">
|
||||
<span id="twitch-tags-too-long-warning">Tag too long • </span><span id="twitch-tags-char-count"></span>/<span id="twitch-tags-char-max"></span> Tags
|
||||
</div>
|
||||
</div>
|
||||
<label class="setting-description">Add up to 10 tags. Each tag can be 25 characters long with no spaces or special characters.</label>
|
||||
<input type="text" id="twitch-tags-input" placeholder="Leave empty to keep current tags">
|
||||
</div>
|
||||
|
||||
<button id="twitch-submit-button" onclick="UpdateTwitchSubmit()">Update</button>
|
||||
</div>
|
||||
|
||||
<!-- Update Kick Popup -->
|
||||
<div id="update-kick-dialog" class="dialog">
|
||||
<button class="dialog-nav-button" onclick="CloseUpdateKickDialog()">×</button>
|
||||
|
||||
<div style="display: flex; flex-direction: row; align-items: center; justify-content: center; gap: 1em;">
|
||||
<img src="icons/platforms/kick.png" class="platform-icon">
|
||||
<label class="title" style="text-align: center;">Stream Info</label>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<div style="display: flex; flex-direction: row; align-items: end; gap: 1em;">
|
||||
<label>Title</label>
|
||||
<div id="kick-title-char-limit" class="char-limit">
|
||||
<span id="kick-title-char-count"></span>/<span id="kick-title-char-max"></span>
|
||||
</div>
|
||||
</div>
|
||||
<input type="text" id="kick-title-input" placeholder="Leave empty to keep current title">
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label>Category</label>
|
||||
<input type="text" id="kick-category-input" placeholder="Leave empty to keep current category">
|
||||
</div>
|
||||
|
||||
<button id="kick-submit-button" onclick="UpdateKickSubmit()">Update</button>
|
||||
</div>
|
||||
|
||||
<!-- Update YouTube Popup -->
|
||||
<div id="update-youtube-dialog" class="dialog">
|
||||
<button class="dialog-nav-button" onclick="CloseUpdateYouTubeDialog()">×</button>
|
||||
|
||||
<div style="display: flex; flex-direction: row; align-items: center; justify-content: center; gap: 1em;">
|
||||
<img src="icons/platforms/youtube.png" class="platform-icon">
|
||||
<label class="title" style="text-align: center;">Stream Info</label>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<div style="display: flex; flex-direction: row; align-items: end; gap: 1em;">
|
||||
<label>Title</label>
|
||||
<div id="youtube-title-char-limit" class="char-limit">
|
||||
<span id="youtube-title-char-count"></span>/<span id="youtube-title-char-max"></span>
|
||||
</div>
|
||||
</div>
|
||||
<input type="text" id="youtube-title-input" placeholder="Leave empty to keep current title">
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<div style="display: flex; flex-direction: row; align-items: end; gap: 1em;">
|
||||
<label>Description</label>
|
||||
<div id="youtube-description-char-limit" class="char-limit">
|
||||
<span id="youtube-description-char-count"></span>/<span id="youtube-description-char-max"></span>
|
||||
</div>
|
||||
</div>
|
||||
<textarea type="text" id="youtube-description-input" placeholder="Leave empty to keep current description"
|
||||
class="textarea-description"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label>Category</label>
|
||||
<select id="youtube-category-input">
|
||||
<option value="Autos & Vehicles">Autos & Vehicles</option>
|
||||
<option value="Comedy">Comedy</option>
|
||||
<option value="Education">Education</option>
|
||||
<option value="Entertainment">Entertainment</option>
|
||||
<option value="Film & Animation">Film & Animation</option>
|
||||
<option value="Gaming">Gaming</option>
|
||||
<option value="Howto & Style">Howto & Style</option>
|
||||
<option value="Music">Music</option>
|
||||
<option value="News & Politics">News & Politics</option>
|
||||
<option value="Nonprofits & Activism">Nonprofits & Activism</option>
|
||||
<option value="People & Blogs">People & Blogs</option>
|
||||
<option value="Pets & Animals">Pets & Animals</option>
|
||||
<option value="Science & Technology">Science & Technology</option>
|
||||
<option value="Sports">Sports</option>
|
||||
<option value="Travel & Events">Travel & Events</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<div style="display: flex; flex-direction: row; align-items: end; gap: 1em;">
|
||||
<label>Tags</label>
|
||||
<div id="youtube-tags-char-limit" class="char-limit">
|
||||
<span id="youtube-tags-char-count"></span>/<span id="youtube-tags-char-max"></span>
|
||||
</div>
|
||||
</div>
|
||||
<input type="text" id="youtube-tags-input" placeholder="Leave empty to keep current tags">
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label>Visibility</label>
|
||||
<select id="youtube-privacy-select">
|
||||
<option value="private">Private</option>
|
||||
<option value="unlisted">Unlisted</option>
|
||||
<option value="public">Public</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<button id="youtube-submit-button" onclick="UpdateYouTubeSubmit()">Update</button>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
||||
<!-- Broadcast Template -->
|
||||
<template id="broadcast-template">
|
||||
<div class="broadcast">
|
||||
<img class="platform-icon" id="platform-icon" src="" />
|
||||
|
||||
<div class="broadcast-info">
|
||||
<label id="broadcast-title"></label>
|
||||
<label id="broadcast-category" class="setting-description"></label>
|
||||
<label id="kick-warning" class="setting-attribute"><br><i>Stream info only available when live — Showing
|
||||
last known stream title</i></label>
|
||||
</div>
|
||||
|
||||
<div id="broadcast-buttons">
|
||||
<button id="broadcast-stream-button" class="icon-button" title="Open Stream"><img class="button-icon"
|
||||
src="https://api.iconify.design/streamline:button-play-solid.svg?color=%23ffffff"></button>
|
||||
<button id="broadcast-dashboard-button" class="icon-button" title="Dashboard"><img class="button-icon"
|
||||
src="https://api.iconify.design/gridicons:popout.svg?color=%23ffffff"></button>
|
||||
<button id="broadcast-edit-button" class="icon-button flip-that-shit-homie" title="Edit">✎</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="script.js"></script>
|
||||
@@ -1,546 +0,0 @@
|
||||
//////////////////////
|
||||
// GLOBAL VARIABLES //
|
||||
//////////////////////
|
||||
|
||||
const sbActionFetchBroadcasts = '9486774d-d706-41d8-85b4-7daff5cd1b0d';
|
||||
const sbActionUpdateStreamInfo = '1c58eff0-e98a-4fab-86f0-6f5cee1d3ab3';
|
||||
const sbActionOpenUrl = '14da1d44-6e29-4582-92c3-2c59388be57e';
|
||||
|
||||
let currentBroadcastId = '';
|
||||
let runningActionId = '';
|
||||
|
||||
|
||||
///////////////////
|
||||
// PAGE ELEMENTS //
|
||||
///////////////////
|
||||
|
||||
const blurLayer = document.getElementById('blur-layer');
|
||||
const updateAllDialog = document.getElementById('update-all-dialog');
|
||||
const updateTwitchDialog = document.getElementById('update-twitch-dialog');
|
||||
const updateKickDialog = document.getElementById('update-kick-dialog');
|
||||
const updateYouTubeDialog = document.getElementById('update-youtube-dialog');
|
||||
const broadcastList = document.getElementById('broadcast-list');
|
||||
const youtubeWarning = document.getElementById('youtube-warning');
|
||||
|
||||
|
||||
|
||||
/////////////////////////
|
||||
// STREAMER.BOT EVENTS //
|
||||
/////////////////////////
|
||||
|
||||
window.addEventListener("message", (event) => {
|
||||
FetchBroadcasts();
|
||||
});
|
||||
|
||||
window.parent.sbClient.on('General.Custom', (response) => {
|
||||
console.debug(response.data);
|
||||
GeneralCustom(response.data);
|
||||
})
|
||||
|
||||
|
||||
|
||||
///////////////////////////////
|
||||
// MULTISTREAM TITLE UPDATER //
|
||||
///////////////////////////////
|
||||
|
||||
async function GeneralCustom(data) {
|
||||
// Only run if response matches the ID of the corresponding FetchBroadcasts() call
|
||||
if (runningActionId != data.runningActionId)
|
||||
return;
|
||||
|
||||
switch(data.actionId) {
|
||||
case sbActionFetchBroadcasts:
|
||||
{
|
||||
// Iterate through list of broadcasts and add it to the list
|
||||
for (const broadcast of data.broadcastList)
|
||||
await AddBroadcast(broadcast);
|
||||
|
||||
// Check if YouTube account is connected
|
||||
// If yes, count how many YouTube broadcasts there were
|
||||
// If 0, show warning
|
||||
const broadcasterInfo = await window.parent.sbClient.getBroadcaster();
|
||||
if (broadcasterInfo.platforms.youtube)
|
||||
{
|
||||
// Only show the warning if there are 0 monitored broadcasts
|
||||
const ytBroadcastCount = data.broadcastList.filter(b => b.platform === "youtube").length;
|
||||
if (ytBroadcastCount <= 0)
|
||||
youtubeWarning.style.display = 'flex';
|
||||
else
|
||||
youtubeWarning.style.display = 'none';
|
||||
|
||||
// Set the URL to the livestreaming dashboard
|
||||
const broadcastButton = youtubeWarning.querySelector('#broadcast-dashboard-button');
|
||||
broadcastButton.onclick = function() {
|
||||
//window.open(data.streamUrl, '_blank');
|
||||
OpenURL(`https://studio.youtube.com/channel/${broadcasterInfo.platforms.youtube.broadcastUserId}/livestreaming`);
|
||||
};
|
||||
}
|
||||
else
|
||||
youtubeWarning.style.display = 'none';
|
||||
|
||||
// Check if any broadcasts have gone offline
|
||||
// If so, delete them from the list
|
||||
const childDivs = broadcastList.querySelectorAll(":scope > div");
|
||||
childDivs.forEach(childDiv => {
|
||||
var result = data.broadcastList.find(obj => {
|
||||
return obj.id === childDiv.id
|
||||
})
|
||||
if (result == null)
|
||||
childDiv.remove();
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
async function FetchBroadcasts() {
|
||||
// Fetch from Streamer.bot
|
||||
const response = await window.parent.sbClient.doAction({ id: sbActionFetchBroadcasts});
|
||||
runningActionId = response.args.runningActionId;
|
||||
}
|
||||
|
||||
async function AddBroadcast(data) {
|
||||
// Get a reference to the template
|
||||
const template = document.getElementById('broadcast-template');
|
||||
|
||||
const existingDiv = broadcastList.querySelector(`#${data.id}`);
|
||||
|
||||
// Create a new instance of the template
|
||||
let instance;
|
||||
|
||||
if (existingDiv)
|
||||
instance = existingDiv;
|
||||
else {
|
||||
instance = template.content.firstElementChild.cloneNode(true);
|
||||
instance.id = data.id;
|
||||
broadcastList.appendChild(instance);
|
||||
}
|
||||
|
||||
// Get divs
|
||||
const platformIconEl = instance.querySelector('#platform-icon');
|
||||
const titleEl = instance.querySelector('#broadcast-title');
|
||||
const categoryEl = instance.querySelector('#broadcast-category');
|
||||
const kickWarningEl = instance.querySelector('#kick-warning');
|
||||
const streamButtonEl = instance.querySelector('#broadcast-stream-button');
|
||||
const dashboardButtonEl = instance.querySelector('#broadcast-dashboard-button');
|
||||
const editButtonEl = instance.querySelector('#broadcast-edit-button');
|
||||
|
||||
// Streamer.bot does not provide title/category for Kick, so pull from API
|
||||
if (data.platform == 'kick')
|
||||
{
|
||||
let response = await fetch('https://kick.com/api/v1/channels/' + data.userLogin);
|
||||
let response_data = await response.json();
|
||||
|
||||
// The current title is only provided if the stream if currently live
|
||||
if (response_data.livestream)
|
||||
{
|
||||
data.title = response_data.livestream.session_title;
|
||||
data.category = response_data.livestream.categories[0].name;
|
||||
kickWarningEl.style.display = 'none';
|
||||
}
|
||||
else if (response_data.previous_livestreams.length > 0)
|
||||
{
|
||||
data.title = response_data.previous_livestreams[0].session_title;
|
||||
data.category = response_data.previous_livestreams[0].categories[0].name;
|
||||
kickWarningEl.style.display = 'inline';
|
||||
}
|
||||
else
|
||||
{
|
||||
data.title = '';
|
||||
data.category = '';
|
||||
kickWarningEl.style.display = 'inline';
|
||||
}
|
||||
}
|
||||
|
||||
// Flash green to show that it updated
|
||||
if (titleEl.textContent != data.title || categoryEl.textContent != data.category)
|
||||
{
|
||||
instance.style.backgroundColor = getComputedStyle(document.documentElement).getPropertyValue('--confirm-flash');
|
||||
setTimeout(() => {
|
||||
instance.style.backgroundColor = '';
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
// Set the platform icon
|
||||
platformIconEl.src = `icons/platforms/${data.platform}.png`;
|
||||
|
||||
// Special logo for YouTube shorts
|
||||
if (data.platform == 'youtube') {
|
||||
const targets = ["vertical", "shorts"];
|
||||
|
||||
const isShort = data.tags.some(item =>
|
||||
targets.some(target => item.toLowerCase() === target.toLowerCase())
|
||||
);
|
||||
if (isShort)
|
||||
platformIconEl.src = `icons/platforms/youtube-shorts.png`;
|
||||
}
|
||||
|
||||
// Set the stream title
|
||||
if (data.title)
|
||||
titleEl.textContent = data.title;
|
||||
|
||||
// Set the stream category
|
||||
if (data.category)
|
||||
categoryEl.textContent = data.category;
|
||||
|
||||
streamButtonEl.onclick = function() {
|
||||
//window.open(data.streamUrl, '_blank');
|
||||
OpenURL(data.streamUrl);
|
||||
};
|
||||
|
||||
dashboardButtonEl.onclick = function() {
|
||||
//window.open(data.dashboardUrl, '_blank');
|
||||
OpenURL(data.dashboardUrl);
|
||||
};
|
||||
|
||||
editButtonEl.onclick = function() {
|
||||
switch (data.platform) {
|
||||
case 'twitch':
|
||||
document.getElementById('twitch-title-input').value = data.title;
|
||||
document.getElementById('twitch-category-input').value = data.category;
|
||||
document.getElementById('twitch-tags-input').value = data.tags.join(", ");
|
||||
ValidateTwitchDialog();
|
||||
updateTwitchDialog.style.display = "flex";
|
||||
break;
|
||||
case 'kick':
|
||||
document.getElementById('kick-title-input').value = titleEl.textContent;
|
||||
document.getElementById('kick-category-input').value = categoryEl.textContent;
|
||||
ValidateKickDialog();
|
||||
updateKickDialog.style.display = "flex";
|
||||
break;
|
||||
case 'youtube':
|
||||
currentBroadcastId = data.id;
|
||||
document.getElementById('youtube-title-input').value = data.title;
|
||||
document.getElementById('youtube-description-input').value = data.description;
|
||||
document.getElementById('youtube-category-input').value = data.category;
|
||||
document.getElementById('youtube-tags-input').value = data.tags.join(", ");
|
||||
document.getElementById('youtube-privacy-select').value = data.privacy;
|
||||
ValidateYouTubeDialog();
|
||||
updateYouTubeDialog.style.display = "flex";
|
||||
break;
|
||||
}
|
||||
blurLayer.style.display = "block";
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
//////////////////////
|
||||
// HELPER FUNCTIONS //
|
||||
//////////////////////
|
||||
|
||||
async function OpenURL(url) {
|
||||
await window.parent.sbClient.doAction(
|
||||
action = {
|
||||
id: sbActionOpenUrl
|
||||
},
|
||||
args = {
|
||||
url: url
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////
|
||||
// PAGE INTERACTIONS //
|
||||
///////////////////////
|
||||
|
||||
function OpenUpdateAllDialog() {
|
||||
updateAllDialog.style.display = "flex";
|
||||
blurLayer.style.display = "block";
|
||||
}
|
||||
|
||||
function CloseUpdateAllDialog() {
|
||||
updateAllDialog.style.display = "none";
|
||||
blurLayer.style.display = "none";
|
||||
}
|
||||
|
||||
function CloseUpdateTwitchDialog() {
|
||||
updateTwitchDialog.style.display = "none";
|
||||
blurLayer.style.display = "none";
|
||||
}
|
||||
|
||||
function CloseUpdateKickDialog() {
|
||||
updateKickDialog.style.display = "none";
|
||||
blurLayer.style.display = "none";
|
||||
}
|
||||
|
||||
function CloseUpdateYouTubeDialog() {
|
||||
updateYouTubeDialog.style.display = "none";
|
||||
blurLayer.style.display = "none";
|
||||
}
|
||||
|
||||
async function UpdateAllSubmit() {
|
||||
await window.parent.sbClient.doAction(
|
||||
action = {
|
||||
id: sbActionUpdateStreamInfo
|
||||
},
|
||||
args = {
|
||||
platform: 'all',
|
||||
title: document.getElementById('all-title-input').value,
|
||||
category: document.getElementById('all-category-input').value
|
||||
}
|
||||
);
|
||||
|
||||
CloseUpdateAllDialog();
|
||||
}
|
||||
|
||||
async function UpdateTwitchSubmit() {
|
||||
await window.parent.sbClient.doAction(
|
||||
action = {
|
||||
id: sbActionUpdateStreamInfo
|
||||
},
|
||||
args = {
|
||||
platform: 'twitch',
|
||||
title: document.getElementById('twitch-title-input').value,
|
||||
category: document.getElementById('twitch-category-input').value,
|
||||
tags: document.getElementById('twitch-tags-input').value
|
||||
}
|
||||
);
|
||||
|
||||
CloseUpdateTwitchDialog();
|
||||
}
|
||||
|
||||
async function UpdateKickSubmit() {
|
||||
await window.parent.sbClient.doAction(
|
||||
action = {
|
||||
id: sbActionUpdateStreamInfo
|
||||
},
|
||||
args = {
|
||||
platform: 'kick',
|
||||
title: document.getElementById('kick-title-input').value,
|
||||
category: document.getElementById('kick-category-input').value
|
||||
}
|
||||
);
|
||||
|
||||
CloseUpdateKickDialog();
|
||||
}
|
||||
|
||||
async function UpdateYouTubeSubmit() {
|
||||
await window.parent.sbClient.doAction(
|
||||
action = {
|
||||
id: sbActionUpdateStreamInfo
|
||||
},
|
||||
args = {
|
||||
platform: 'youtube',
|
||||
title: document.getElementById('youtube-title-input').value,
|
||||
description: document.getElementById('youtube-description-input').value,
|
||||
category: document.getElementById('youtube-category-input').value,
|
||||
tags: document.getElementById('youtube-tags-input').value,
|
||||
privacy: document.getElementById('youtube-privacy-select').value,
|
||||
broadcastId: currentBroadcastId
|
||||
}
|
||||
);
|
||||
|
||||
CloseUpdateYouTubeDialog();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/////////////////////
|
||||
// DATA VALIDATION //
|
||||
/////////////////////
|
||||
|
||||
const GLOBAL_TITLE_MAX = 100;
|
||||
const TWITCH_TITLE_MAX = 140;
|
||||
const TWITCH_TAGS_MAX = 10;
|
||||
const KICK_TITLE_MAX = 100;
|
||||
const YOUTUBE_TITLE_MAX = 100;
|
||||
const YOUTUBE_DESCRIPTION_MAX = 5000;
|
||||
const YOUTUBE_TAGS_MAX = 500;
|
||||
|
||||
const allTitleInput = document.getElementById("all-title-input");
|
||||
const twitchTitleInput = document.getElementById("twitch-title-input");
|
||||
const twitchTagsInput = document.getElementById("twitch-tags-input");
|
||||
const kickTitleInput = document.getElementById("kick-title-input");
|
||||
const youtubeTitleInput = document.getElementById("youtube-title-input");
|
||||
const youtubeDescriptionInput = document.getElementById("youtube-description-input");
|
||||
const youtubeTagsInput = document.getElementById("youtube-tags-input");
|
||||
|
||||
// Function to update char count and handle validation
|
||||
function ValidateUpdateAllDialog() {
|
||||
// Get references to elements
|
||||
const allTitleCharLimit = document.getElementById('all-title-char-limit');
|
||||
const allCharCount = document.getElementById('all-title-char-count');
|
||||
const allTitleCharMax = document.getElementById('all-title-char-max');
|
||||
const allUpdateButton = document.getElementById('all-submit-button');
|
||||
|
||||
// Validate title field
|
||||
const currentLength = allTitleInput.value.length;
|
||||
allCharCount.textContent = currentLength;
|
||||
allTitleCharMax.textContent = GLOBAL_TITLE_MAX;
|
||||
|
||||
if (currentLength > GLOBAL_TITLE_MAX)
|
||||
allTitleCharLimit.style.color = getComputedStyle(document.documentElement).getPropertyValue('--error-text-color');
|
||||
else
|
||||
allTitleCharLimit.style.color = ""; // reset to default
|
||||
|
||||
// Set button interactability (yes, that is a word — look it up)
|
||||
if (currentLength > GLOBAL_TITLE_MAX)
|
||||
allUpdateButton.disabled = true;
|
||||
else
|
||||
allUpdateButton.disabled = false;
|
||||
}
|
||||
|
||||
// Function to update char count and handle validation
|
||||
function ValidateTwitchDialog() {
|
||||
// Get references to elements
|
||||
const twitchTitleCharLimit = document.getElementById('twitch-title-char-limit');
|
||||
const twitchCharCount = document.getElementById('twitch-title-char-count');
|
||||
const twitchTitleCharMax = document.getElementById('twitch-title-char-max');
|
||||
const twitchTagsCharLimit = document.getElementById('twitch-tags-char-limit');
|
||||
const twitchTagsCharCount = document.getElementById('twitch-tags-char-count');
|
||||
const twitchTagsCharMax = document.getElementById('twitch-tags-char-max');
|
||||
const twitchTagsTooLongWarning = document.getElementById('twitch-tags-too-long-warning');
|
||||
const twitchUpdateButton = document.getElementById('twitch-submit-button');
|
||||
|
||||
// Validate title field
|
||||
const currentLength = twitchTitleInput.value.length;
|
||||
twitchCharCount.textContent = currentLength;
|
||||
twitchTitleCharMax.textContent = TWITCH_TITLE_MAX;
|
||||
|
||||
if (currentLength > TWITCH_TITLE_MAX)
|
||||
twitchTitleCharLimit.style.color = getComputedStyle(document.documentElement).getPropertyValue('--error-text-color');
|
||||
else
|
||||
twitchTitleCharLimit.style.color = ""; // reset to default
|
||||
|
||||
// Validate tags field
|
||||
// Split by comma, trim each tag, sum lengths
|
||||
const tagsArray = twitchTagsInput.value
|
||||
.split(',')
|
||||
.map(tag => tag.trim())
|
||||
.filter(tag => tag.length > 0);
|
||||
|
||||
const tagsLength = tagsArray.length;
|
||||
twitchTagsCharCount.textContent = tagsLength;
|
||||
twitchTagsCharMax.textContent = TWITCH_TAGS_MAX;
|
||||
|
||||
if (tagsLength > TWITCH_TAGS_MAX)
|
||||
twitchTagsCharLimit.style.color = getComputedStyle(document.documentElement).getPropertyValue('--error-text-color');
|
||||
else
|
||||
twitchTagsCharLimit.style.color = ""; // reset to default
|
||||
|
||||
// Also check that each tag is under 25 characters
|
||||
const hasLongTag = tagsArray.some(tag => tag.length > 25);
|
||||
if (hasLongTag) {
|
||||
twitchTagsTooLongWarning.style.display = 'inline';
|
||||
twitchTagsCharLimit.style.color = getComputedStyle(document.documentElement).getPropertyValue('--error-text-color');
|
||||
}
|
||||
else {
|
||||
twitchTagsTooLongWarning.style.display = 'none';
|
||||
twitchTagsCharLimit.style.color = ""; // reset to default
|
||||
}
|
||||
|
||||
// Set button interactability (yes, that is a word — look it up)
|
||||
if (currentLength > TWITCH_TITLE_MAX || tagsLength > TWITCH_TAGS_MAX || hasLongTag)
|
||||
twitchUpdateButton.disabled = true;
|
||||
else
|
||||
twitchUpdateButton.disabled = false;
|
||||
}
|
||||
|
||||
// Function to update char count and handle validation
|
||||
function ValidateKickDialog() {
|
||||
// Get references to elements
|
||||
const kickTitleCharLimit = document.getElementById('kick-title-char-limit');
|
||||
const kickCharCount = document.getElementById('kick-title-char-count');
|
||||
const kickTitleCharMax = document.getElementById('kick-title-char-max');
|
||||
const kickUpdateButton = document.getElementById('kick-submit-button');
|
||||
|
||||
// Validate title field
|
||||
const currentLength = kickTitleInput.value.length;
|
||||
kickCharCount.textContent = currentLength;
|
||||
kickTitleCharMax.textContent = KICK_TITLE_MAX;
|
||||
|
||||
if (currentLength > KICK_TITLE_MAX)
|
||||
kickTitleCharLimit.style.color = getComputedStyle(document.documentElement).getPropertyValue('--error-text-color');
|
||||
else
|
||||
kickTitleCharLimit.style.color = ""; // reset to default
|
||||
|
||||
// Set button interactability (yes, that is a word — look it up)
|
||||
if (currentLength > KICK_TITLE_MAX)
|
||||
kickUpdateButton.disabled = true;
|
||||
else
|
||||
kickUpdateButton.disabled = false;
|
||||
}
|
||||
|
||||
function ValidateYouTubeDialog() {
|
||||
// Get references to elements
|
||||
const youtubeTitleCharLimit = document.getElementById('youtube-title-char-limit');
|
||||
const youtubeTitleCharCount = document.getElementById('youtube-title-char-count');
|
||||
const youtubeTitleCharMax = document.getElementById('youtube-title-char-max');
|
||||
const youtubeDescriptionCharLimit = document.getElementById('youtube-description-char-limit');
|
||||
const youtubeDescriptionCharCount = document.getElementById('youtube-description-char-count');
|
||||
const youtubeDescriptionCharMax = document.getElementById('youtube-description-char-max');
|
||||
const youtubeTagsCharLimit = document.getElementById('youtube-tags-char-limit');
|
||||
const youtubeTagsCharCount = document.getElementById('youtube-tags-char-count');
|
||||
const youtubeTagsCharMax = document.getElementById('youtube-tags-char-max');
|
||||
const youtubeUpdateButton = document.getElementById('youtube-submit-button');
|
||||
|
||||
// Validate title field
|
||||
const titleLength = youtubeTitleInput.value.length;
|
||||
youtubeTitleCharCount.textContent = titleLength;
|
||||
youtubeTitleCharMax.textContent = YOUTUBE_TITLE_MAX;
|
||||
|
||||
if (titleLength > YOUTUBE_TITLE_MAX)
|
||||
youtubeTitleCharLimit.style.color = getComputedStyle(document.documentElement).getPropertyValue('--error-text-color');
|
||||
else
|
||||
youtubeTitleCharLimit.style.color = ""; // reset to default
|
||||
|
||||
// Validate description field
|
||||
const descriptionLength = youtubeDescriptionInput.value.length;
|
||||
youtubeDescriptionCharCount.textContent = descriptionLength;
|
||||
youtubeDescriptionCharMax.textContent = YOUTUBE_DESCRIPTION_MAX;
|
||||
|
||||
if (descriptionLength > YOUTUBE_DESCRIPTION_MAX)
|
||||
youtubeDescriptionCharLimit.style.color = getComputedStyle(document.documentElement).getPropertyValue('--error-text-color');
|
||||
else
|
||||
youtubeDescriptionCharLimit.style.color = ""; // reset to default
|
||||
|
||||
// Validate tags field
|
||||
// Split by comma, trim each tag, sum lengths
|
||||
const tagsArray = youtubeTagsInput.value
|
||||
.split(',')
|
||||
.map(tag => tag.trim())
|
||||
.filter(tag => tag.length > 0);
|
||||
|
||||
const tagsLength = tagsArray.reduce((sum, tag) => sum + tag.length, 0) + (tagsArray.length > 0 ? tagsArray.length - 1 : 0);
|
||||
youtubeTagsCharCount.textContent = tagsLength;
|
||||
youtubeTagsCharMax.textContent = YOUTUBE_TAGS_MAX;
|
||||
|
||||
if (tagsLength > YOUTUBE_TAGS_MAX)
|
||||
youtubeTagsCharLimit.style.color = getComputedStyle(document.documentElement).getPropertyValue('--error-text-color');
|
||||
else
|
||||
youtubeTagsCharLimit.style.color = ""; // reset to default
|
||||
|
||||
// Set button interactability (yes, that is a word — look it up)
|
||||
if (titleLength > YOUTUBE_TITLE_MAX || descriptionLength > YOUTUBE_DESCRIPTION_MAX || tagsLength > YOUTUBE_TAGS_MAX)
|
||||
youtubeUpdateButton.disabled = true;
|
||||
else
|
||||
youtubeUpdateButton.disabled = false;
|
||||
}
|
||||
|
||||
// Attach event listener
|
||||
allTitleInput.addEventListener("input", ValidateUpdateAllDialog);
|
||||
twitchTitleInput.addEventListener("input", ValidateTwitchDialog);
|
||||
twitchTagsInput.addEventListener("input", ValidateTwitchDialog);
|
||||
kickTitleInput.addEventListener("input", ValidateKickDialog);
|
||||
youtubeTitleInput.addEventListener("input", ValidateYouTubeDialog);
|
||||
youtubeDescriptionInput.addEventListener("input", ValidateYouTubeDialog);
|
||||
youtubeTagsInput.addEventListener("input", ValidateYouTubeDialog);
|
||||
|
||||
// Initial check (in case the input has prefilled text)
|
||||
ValidateUpdateAllDialog();
|
||||
ValidateTwitchDialog();
|
||||
ValidateKickDialog();
|
||||
ValidateYouTubeDialog();
|
||||
|
||||
|
||||
|
||||
|
||||
////////////////////////////
|
||||
// REFRESH BROADCAST LIST //
|
||||
////////////////////////////
|
||||
|
||||
setInterval(FetchBroadcasts, 5000);
|
||||
@@ -1,85 +0,0 @@
|
||||
body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1em;
|
||||
margin: 1em;
|
||||
}
|
||||
|
||||
#broadcast-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5em;
|
||||
}
|
||||
|
||||
button {
|
||||
gap: 0.5em;
|
||||
}
|
||||
|
||||
.broadcast {
|
||||
background-color: var(--dialog-background);
|
||||
border: 1px solid #40404080;
|
||||
border-radius: 0.5em;
|
||||
padding: 0.2em 0.5em;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 1em;
|
||||
|
||||
transition: 0.4s;
|
||||
}
|
||||
|
||||
.broadcast-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
#broadcast-buttons {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.platform-icon {
|
||||
height: 2em;
|
||||
}
|
||||
|
||||
.button-icon {
|
||||
height: 1em;
|
||||
}
|
||||
|
||||
.flip-that-shit-homie {
|
||||
/* Flip that bad boy */
|
||||
transform: scaleX(-1);
|
||||
}
|
||||
|
||||
.blur {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.dialog {
|
||||
display: none;
|
||||
width: 90%;
|
||||
max-height: 80%;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.setting-attribute {
|
||||
color: yellow;
|
||||
}
|
||||
|
||||
#kick-warning {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#youtube-warning {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.char-limit {
|
||||
margin-left: auto;
|
||||
text-align: right;
|
||||
font-size: 0.8em;
|
||||
opacity: 0.7;
|
||||
}
|
||||
@@ -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 |
@@ -1,20 +1,81 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="stylesheet" href="../.common/styles/global.css" />
|
||||
<link rel="icon" href="../../.resources/logo.png" type="image/png">
|
||||
<title>nutty</title>
|
||||
<style>
|
||||
#dock-wrapper {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
}
|
||||
</style>
|
||||
<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>
|
||||
<iframe id="dock-wrapper"></iframe>
|
||||
<script src="script.js"></script>
|
||||
<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>
|
||||
|
||||
</html>
|
||||
<script src='https://cdnjs.cloudflare.com/ajax/libs/gsap/1.18.0/TweenMax.min.js'></script>
|
||||
<script src="./script.js"></script>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg width="100%" height="100%" viewBox="0 0 2000 1556" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||
<path d="M1000,0C1429.31,0 1777.86,348.548 1777.86,777.862C1777.86,1207.18 1429.31,1555.72 1000,1555.72C570.686,1555.72 222.138,1207.18 222.138,777.862C222.138,348.548 570.686,0 1000,0ZM934,714.686L934,1145L865,1145L865,1216.69L1137.63,1216.69L1137.63,1145L1068.63,1145L1068.63,643L865,643L865,714.686L934,714.686ZM1000,305C943.705,305 898,350.705 898,407C898,463.295 943.705,509 1000,509C1056.3,509 1102,463.295 1102,407C1102,350.705 1056.3,305 1000,305Z" style="fill:white;"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 937 B |
@@ -0,0 +1,4 @@
|
||||
[NUT] Multistream Title Updater | Fetch Broadcasts
|
||||
[NUT] Multistream Title Updater | Update All Broadcasts
|
||||
[NUT] Multistream Title Updater | Update Twitch Title
|
||||
[NUT] Multistream Title Updater | Update YouTube Title
|
||||
@@ -1,16 +1,444 @@
|
||||
// Construct URL
|
||||
const currentURL = window.location.href;
|
||||
let baseURL = currentURL;
|
||||
////////////
|
||||
// FIELDS //
|
||||
////////////
|
||||
|
||||
if (baseURL.endsWith("index.html"))
|
||||
baseURL = baseURL.replace("index.html", "");
|
||||
let sbDebugMode = true;
|
||||
|
||||
const configJson = "?config=" + baseURL + "config.json";
|
||||
const queryString = window.location.search;
|
||||
const urlParams = new URLSearchParams(queryString);
|
||||
|
||||
// Implement widget dock core
|
||||
window.dockWrapper = document.getElementById('dock-wrapper');
|
||||
dockWrapper.src = `../../.common/core/widget-dock-core${configJson}`;
|
||||
const sbServerPort = urlParams.get("port") || 8080;
|
||||
const sbServerAddress = urlParams.get("server") || "127.0.0.1";
|
||||
|
||||
dockWrapper.addEventListener('load', () => {
|
||||
dockWrapper.contentWindow.content.src = baseURL + '/contents';
|
||||
});
|
||||
/////////////////
|
||||
// GLOBAL VARS //
|
||||
/////////////////
|
||||
|
||||
let ws;
|
||||
|
||||
///////////////////////////////////
|
||||
// SRTEAMER.BOT WEBSOCKET SERVER //
|
||||
///////////////////////////////////
|
||||
|
||||
// This is the main function that connects to the Streamer.bot websocke server
|
||||
function connectws() {
|
||||
if ("WebSocket" in window) {
|
||||
ws = new WebSocket("ws://" + sbServerAddress + ":" + sbServerPort + "/");
|
||||
|
||||
// Reconnect
|
||||
ws.onclose = function () {
|
||||
SetConnectionStatus(false);
|
||||
setTimeout(connectws, 5000);
|
||||
};
|
||||
|
||||
// Connect
|
||||
ws.onopen = async function () {
|
||||
SetConnectionStatus(true);
|
||||
|
||||
console.log("Subscribe to events");
|
||||
ws.send(
|
||||
JSON.stringify({
|
||||
request: "Subscribe",
|
||||
id: "subscribe-events-id",
|
||||
// This is the list of Streamer.bot websocket events to subscribe to
|
||||
// See full list of events here:
|
||||
// https://docs.streamer.bot/api/servers/websocket/requests
|
||||
events: {
|
||||
twitch: [
|
||||
"StreamUpdate"
|
||||
],
|
||||
youTube: [
|
||||
"BroadcastStarted",
|
||||
"BroadcastEnded",
|
||||
"BroadcastUpdated"
|
||||
],
|
||||
general: [
|
||||
"Custom"
|
||||
]
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
sbGetActions(ws);
|
||||
|
||||
ws.onmessage = function (event) {
|
||||
// Grab message and parse JSON
|
||||
const msg = event.data;
|
||||
const wsdata = JSON.parse(msg);
|
||||
|
||||
// Check if the user installed all the required Streamer.bot actions
|
||||
if (wsdata.id == "GetActions") {
|
||||
|
||||
// Check if all the required SB action exist
|
||||
ReadLinesFromFile('requiredActions.txt')
|
||||
.then(requiredActions => {
|
||||
if (sbCheckRequiredActions(wsdata.actions, requiredActions)) {
|
||||
SetElementVisibility("missingActionsInstructions", false);
|
||||
sbFetchBroadcasts(ws);
|
||||
}
|
||||
else
|
||||
SetElementVisibility("missingActionsInstructions", true);
|
||||
})
|
||||
}
|
||||
|
||||
if (typeof wsdata.event == "undefined") {
|
||||
return;
|
||||
}
|
||||
|
||||
// Print data to log for debugging purposes
|
||||
if (sbDebugMode) {
|
||||
console.log(wsdata.data);
|
||||
console.log(wsdata.event.source);
|
||||
console.log(wsdata.event.type);
|
||||
}
|
||||
|
||||
// Check for events to trigger
|
||||
// See documentation for all events here:
|
||||
// https://wiki.streamer.bot/en/Servers-Clients/WebSocket-Server/Events
|
||||
switch (wsdata.event.source) {
|
||||
// Twitch Events
|
||||
case 'Twitch':
|
||||
switch (wsdata.event.type) {
|
||||
case ('StreamUpdate'):
|
||||
sbFetchBroadcasts(ws);
|
||||
break;
|
||||
}
|
||||
// Twitch Events
|
||||
case 'YouTube':
|
||||
switch (wsdata.event.type) {
|
||||
case ('BroadcastStarted'):
|
||||
case ('BroadcastEnded'):
|
||||
case ('BroadcastUpdated'):
|
||||
sbFetchBroadcasts(ws);
|
||||
break;
|
||||
}
|
||||
// General Events
|
||||
case 'General':
|
||||
switch (wsdata.event.type) {
|
||||
case ('Custom'):
|
||||
switch (wsdata.data.action) {
|
||||
case ('[NUT] Multistream Title Updater | Fetch Broadcasts'):
|
||||
UpdateBroadcastList(wsdata.data);
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
break;
|
||||
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/////////////////////////
|
||||
// STREAMER.BOT WIDGET //
|
||||
/////////////////////////
|
||||
|
||||
function sbGetActions(ws) {
|
||||
|
||||
let request = JSON.stringify({
|
||||
request: "GetActions",
|
||||
id: "GetActions"
|
||||
});
|
||||
|
||||
ws.send(request);
|
||||
}
|
||||
|
||||
// Check if all the entries in targetActionNames exist in actionList
|
||||
function sbCheckRequiredActions(actionList, targetActionNames) {
|
||||
let foundActions = 0;
|
||||
for (targetActionName of targetActionNames) {
|
||||
if (actionList.some(action => action.name === targetActionName))
|
||||
foundActions++;
|
||||
}
|
||||
|
||||
return foundActions == targetActionNames.length
|
||||
}
|
||||
|
||||
function sbFetchBroadcasts(ws) {
|
||||
|
||||
let request = JSON.stringify({
|
||||
request: "DoAction",
|
||||
id: generateUUID(),
|
||||
action: {
|
||||
name: "[NUT] Multistream Title Updater | Fetch Broadcasts"
|
||||
}
|
||||
});
|
||||
|
||||
ws.send(request);
|
||||
}
|
||||
|
||||
function sbUpdateTitles(ws, title) {
|
||||
let request = JSON.stringify({
|
||||
request: "DoAction",
|
||||
id: generateUUID(),
|
||||
action: {
|
||||
name: "[NUT] Multistream Title Updater | Update All Broadcasts"
|
||||
},
|
||||
args: {
|
||||
title: title
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
ws.send(request);
|
||||
}
|
||||
|
||||
function sbUpdateTwitchTitle(ws, title) {
|
||||
let request = JSON.stringify({
|
||||
request: "DoAction",
|
||||
id: generateUUID(),
|
||||
action: {
|
||||
name: "[NUT] Multistream Title Updater | Update Twitch Title"
|
||||
},
|
||||
args: {
|
||||
title: title
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
ws.send(request);
|
||||
}
|
||||
|
||||
function sbUpdateYouTubeTitle(ws, title, broadcastId) {
|
||||
let request = JSON.stringify({
|
||||
request: "DoAction",
|
||||
id: generateUUID(),
|
||||
action: {
|
||||
name: "[NUT] Multistream Title Updater | Update YouTube Title"
|
||||
},
|
||||
args: {
|
||||
broadcastId: broadcastId,
|
||||
title: title
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
ws.send(request);
|
||||
}
|
||||
|
||||
|
||||
//////////////////////
|
||||
// HELPER FUNCTIONS //
|
||||
//////////////////////
|
||||
|
||||
function generateUUID() { // Public Domain/MIT
|
||||
var d = new Date().getTime();//Timestamp
|
||||
var d2 = ((typeof performance !== 'undefined') && performance.now && (performance.now() * 1000)) || 0;//Time in microseconds since page-load or 0 if unsupported
|
||||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
|
||||
var r = Math.random() * 16;//random number between 0 and 16
|
||||
if (d > 0) {//Use timestamp until depleted
|
||||
r = (d + r) % 16 | 0;
|
||||
d = Math.floor(d / 16);
|
||||
} else {//Use microseconds since page-load if supported
|
||||
r = (d2 + r) % 16 | 0;
|
||||
d2 = Math.floor(d2 / 16);
|
||||
}
|
||||
return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16);
|
||||
});
|
||||
}
|
||||
|
||||
function IsNullOrWhitespace(str) {
|
||||
return /^\s*$/.test(str);
|
||||
}
|
||||
|
||||
function SetElementVisibility(elementID, visibility) {
|
||||
let element = document.getElementById(elementID);
|
||||
if (visibility)
|
||||
element.style.display = 'inline';
|
||||
else
|
||||
element.style.display = 'none';
|
||||
}
|
||||
|
||||
function ReadLinesFromFile(filePath) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = new XMLHttpRequest();
|
||||
request.open('GET', filePath, true);
|
||||
request.onload = function () {
|
||||
if (request.status === 200) {
|
||||
resolve(request.responseText.split(/\r?\n/));
|
||||
} else {
|
||||
reject(new Error(`File loading failed with status: ${request.status}`));
|
||||
}
|
||||
};
|
||||
request.onerror = function () {
|
||||
reject(new Error('Network error occurred during file loading.'));
|
||||
};
|
||||
request.send();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////////////
|
||||
// STREAMER.BOT WEBSOCKET STATUS //
|
||||
///////////////////////////////////
|
||||
|
||||
// This function sets the visibility of the Streamer.bot status label on the overlay
|
||||
function SetConnectionStatus(connected) {
|
||||
let connectionStatusIcon = document.getElementById("connectionStatusIcon");
|
||||
let ThisIsWhereAllTheCoolStuffHappens = document.getElementById("ThisIsWhereAllTheCoolStuffHappens");
|
||||
|
||||
if (connected) {
|
||||
connectionStatusIcon.src = "connected.svg";
|
||||
ThisIsWhereAllTheCoolStuffHappens.classList.remove('disabled');
|
||||
SetElementVisibility("infoIcon", false);
|
||||
SetElementVisibility("streamerbotConnectInstructions", false);
|
||||
}
|
||||
else {
|
||||
connectionStatusIcon.src = "disconnected.svg";
|
||||
ThisIsWhereAllTheCoolStuffHappens.classList.add('disabled');
|
||||
SetElementVisibility("infoIcon", true);
|
||||
SetElementVisibility("streamerbotConnectInstructions", true);
|
||||
|
||||
// (1) Clear list of broadcasts
|
||||
const fieldsContainer = document.getElementById('fieldsContainer');
|
||||
fieldsContainer.innerHTML = "";
|
||||
}
|
||||
}
|
||||
|
||||
// This function sets the visibility of the Streamer.bot status label on the overlay
|
||||
function RefreshAnimation() {
|
||||
let refreshContainer = document.getElementById("refreshContainer");
|
||||
refreshContainer.style.opacity = 1;
|
||||
var tl = new TimelineMax();
|
||||
tl
|
||||
.to(refreshContainer, 2, { opacity: 0, ease: Linear.easeNone })
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Button handling for UPDATE ALL button only
|
||||
const infoIcon = document.querySelector("#infoIcon");
|
||||
infoIcon.addEventListener("click", function () {
|
||||
window.open("https://www.notion.so/nutty-s-multistream-title-updater-e2fb7b24c7514f9cbd943729f54be1d9");
|
||||
});
|
||||
|
||||
// Button handling for UPDATE ALL button only
|
||||
const submitButton = document.querySelector("#submitButton");
|
||||
submitButton.addEventListener("click", function () {
|
||||
const titleInput = document.querySelector("#titleInput").value;
|
||||
sbUpdateTitles(ws, titleInput);
|
||||
});
|
||||
|
||||
// Button handling for REFRESH button only
|
||||
const refreshButton = document.querySelector("#refreshButton");
|
||||
refreshButton.addEventListener("click", function () {
|
||||
sbFetchBroadcasts(ws);
|
||||
});
|
||||
|
||||
|
||||
function UpdateBroadcastList(data) {
|
||||
// Iterate through broadcast list
|
||||
// Find div whose ID matches the broadcast
|
||||
// If it exists, update the title
|
||||
// else, it's a new broadcast, so add it to the list
|
||||
const fieldsContainer = document.getElementById('fieldsContainer');
|
||||
for (const broadcast of data.broadcastList) {
|
||||
const broadcastDiv = fieldsContainer.querySelector(`#${broadcast.id}`);
|
||||
|
||||
if (broadcastDiv == null)
|
||||
AddBroadcast(broadcast);
|
||||
else
|
||||
UpdateBroadcast(broadcastDiv, broadcast);
|
||||
}
|
||||
|
||||
// Check if any broadcasts have gone offline
|
||||
// If so, delete them from the list
|
||||
var parentDiv = document.getElementById('fieldsContainer');
|
||||
const childDivs = parentDiv.querySelectorAll("div");
|
||||
childDivs.forEach(childDiv => {
|
||||
var result = data.broadcastList.find(obj => {
|
||||
return obj.id === childDiv.id
|
||||
})
|
||||
if (result == null)
|
||||
childDiv.innerHTML = "";
|
||||
});
|
||||
|
||||
//// THIS CODE ALSO WORKS AND IS WAY SIMPLER, I JUST MADE THINGS
|
||||
//// 10 TIMES HARDER FOR MYSELF BECAUSE I LOVE PAIN POGGERS
|
||||
// // (1) Clear list of broadcasts
|
||||
// const fieldsContainer = document.getElementById('fieldsContainer');
|
||||
// fieldsContainer.innerHTML = "";
|
||||
|
||||
// // (2) For each broadcast in list, create new entry
|
||||
// for (const broadcast of data.broadcastList)
|
||||
// {
|
||||
// AddBroadcast(broadcast);
|
||||
// }
|
||||
|
||||
// Count the number of YouTube broadcasts
|
||||
// If this is 0, put a message to tell the user that they need to go live on YouTube first
|
||||
// Else, hide that message
|
||||
const youtubeBroadcastCount = data.broadcastList.reduce((count, broadcast) => count + (broadcast.platform === "youtube" ? 1 : 0), 0);
|
||||
if (youtubeBroadcastCount <= 0)
|
||||
SetElementVisibility("noYouTubeStreamsInstructions", true);
|
||||
else
|
||||
SetElementVisibility("noYouTubeStreamsInstructions", false);
|
||||
|
||||
RefreshAnimation();
|
||||
}
|
||||
|
||||
function AddBroadcast(broadcast) {
|
||||
// Get a reference to the template
|
||||
const template = document.getElementById('platformTemplate');
|
||||
|
||||
// Create a new instance of the template
|
||||
const instance = template.content.cloneNode(true);
|
||||
|
||||
// Assign ID to instance
|
||||
const broadcastBox = instance.querySelector('.broadcastBox');
|
||||
broadcastBox.id = broadcast.id;
|
||||
|
||||
// Modify the content of the template instance
|
||||
const titleElement = instance.querySelector('.platformTitle');
|
||||
titleElement.value = broadcast.title;
|
||||
|
||||
// Modify the content of the template instance
|
||||
const buttonElement = instance.querySelector('.platformSubmitButton');
|
||||
buttonElement.classList.add(broadcast.platform);
|
||||
|
||||
// Modify the icon of the template instance
|
||||
const iconElement = instance.querySelector('#icon');
|
||||
|
||||
// Add button handling (need separate handling for Twitch/YouTube)
|
||||
switch (broadcast.platform) {
|
||||
case 'twitch':
|
||||
iconElement.src = 'twitch.png';
|
||||
|
||||
buttonElement.addEventListener("click", function () {
|
||||
sbUpdateTwitchTitle(ws, titleElement.value);
|
||||
});
|
||||
break;
|
||||
case 'youtube':
|
||||
// Modify the icon of the template instance
|
||||
iconElement.src = 'youtube.png';
|
||||
|
||||
buttonElement.addEventListener("click", function () {
|
||||
const youtubeID = broadcast.id.replace('youtube-', '');
|
||||
sbUpdateYouTubeTitle(ws, titleElement.value, youtubeID);
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
// Add click event
|
||||
const iconButton = instance.querySelector('#platformIconButton');
|
||||
iconButton.addEventListener("click", function () {
|
||||
window.open(broadcast.url);
|
||||
});
|
||||
|
||||
// Insert the modified template instance into the DOM
|
||||
const fieldsContainer = document.getElementById('fieldsContainer');
|
||||
fieldsContainer.appendChild(instance);
|
||||
}
|
||||
|
||||
function UpdateBroadcast(div, broadcast) {
|
||||
// Modify the content of the template instance
|
||||
const titleElement = div.querySelector('.platformTitle');
|
||||
titleElement.value = broadcast.title;
|
||||
}
|
||||
|
||||
connectws();
|
||||
@@ -0,0 +1,201 @@
|
||||
/* @import url('https://fonts.googleapis.com/css?family={{googleFont}}:100,200,300,400,500,600,700,800,900'); */
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica, Arial, sans-serif;
|
||||
color: rgb(235, 235, 235);
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #1f1f1f;
|
||||
}
|
||||
|
||||
#refreshContainer {
|
||||
font-family: 'Metropolis';
|
||||
font-style: italic;
|
||||
font-weight: bold;
|
||||
font-size: 30px;
|
||||
width: 400px;
|
||||
max-width: fit-content;
|
||||
text-align: center;
|
||||
background-color: #2FB774;
|
||||
opacity: 0;
|
||||
color: white;
|
||||
padding: 10px;
|
||||
border-radius: 10px;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
#instructionsContainer {
|
||||
background-color: rgb(59, 59, 59);
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
border-radius: 10px;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
text-align: center;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.hyperlinks {
|
||||
color: rgb(79, 92, 212);
|
||||
}
|
||||
|
||||
#mainContainer
|
||||
{
|
||||
text-transform: uppercase;
|
||||
max-width: 500px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
padding: 20px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.disabled
|
||||
{
|
||||
pointer-events: none;
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
#title
|
||||
{
|
||||
flex-grow: 1;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
#connectionStatusIcon
|
||||
{
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
#infoIcon
|
||||
{
|
||||
background-color: transparent;
|
||||
background-repeat: no-repeat;
|
||||
border: none;
|
||||
overflow: hidden;
|
||||
outline: none;
|
||||
opacity: 0.5;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
#infoIcon:hover
|
||||
{
|
||||
opacity: 1;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.textBox
|
||||
{
|
||||
padding: 5px 15px;
|
||||
border-radius:20px;
|
||||
background:rgba(255,255,255,.1);
|
||||
border-width: 0;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.straightUpIloveCum
|
||||
{
|
||||
display: flex;
|
||||
margin: 0px 0px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.button
|
||||
{
|
||||
border: none;
|
||||
padding: 5px 15px;
|
||||
border-radius:25px;
|
||||
}
|
||||
|
||||
.button:hover
|
||||
{
|
||||
box-shadow: 0 0px 10px 0 rgb(255, 255, 255, 0.3);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.submitButton
|
||||
{
|
||||
background:rgba(17, 97, 238, 0.2);
|
||||
}
|
||||
|
||||
.submitButton:hover{
|
||||
background-color: rgba(17, 97, 238, 1);
|
||||
}
|
||||
|
||||
#refreshButton
|
||||
{
|
||||
background:rgba(255,255,255,.1);
|
||||
margin: 0px 0px 0px 10px;
|
||||
}
|
||||
|
||||
#refreshButton:hover{
|
||||
background:rgba(255,255,255,.5);
|
||||
}
|
||||
|
||||
#fieldsContainer
|
||||
{
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.broadcastBox
|
||||
{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 10px 0px;
|
||||
}
|
||||
|
||||
.platformTitle
|
||||
{
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.platformSubmitButton
|
||||
{
|
||||
width: 100px;
|
||||
margin: 0px 0px 0px 10px;
|
||||
}
|
||||
|
||||
#platformIconButton
|
||||
{
|
||||
background-color: transparent;
|
||||
background-repeat: no-repeat;
|
||||
border: none;
|
||||
overflow: hidden;
|
||||
outline: none;
|
||||
opacity: 0.5;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
#platformIconButton:hover
|
||||
{
|
||||
opacity: 1;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.twitch
|
||||
{
|
||||
background: rgb(110, 85, 171, 0.5);
|
||||
}
|
||||
|
||||
.twitch:hover
|
||||
{
|
||||
background: rgb(110, 85, 171, 1);
|
||||
}
|
||||
|
||||
.youtube
|
||||
{
|
||||
background: rgb(255, 3, 3, 0.5);
|
||||
}
|
||||
|
||||
.youtube:hover
|
||||
{
|
||||
background: rgb(255, 3, 3, 1);
|
||||
}
|
||||
|
After Width: | Height: | Size: 55 KiB |
|
After Width: | Height: | Size: 65 KiB |