diff --git a/.common/core/widget-dock-core/index.html b/.common/core/widget-dock-core/index.html
index 37638a2..4aa341f 100644
--- a/.common/core/widget-dock-core/index.html
+++ b/.common/core/widget-dock-core/index.html
@@ -16,9 +16,9 @@
@@ -29,7 +29,7 @@
-
diff --git a/.common/core/widget-dock-core/script.js b/.common/core/widget-dock-core/script.js
index dbe6296..e67d58f 100644
--- a/.common/core/widget-dock-core/script.js
+++ b/.common/core/widget-dock-core/script.js
@@ -7,6 +7,7 @@ const urlParams = new URLSearchParams(queryString);
const configJson = urlParams.get("config") || "";
+
///////////////////
// PAGE ELEMENTS //
///////////////////
@@ -22,18 +23,25 @@ const sbRequiredActionsSuccessLabel = document.getElementById('sb-required-actio
const sbRequiredActionsFailureLabel = document.getElementById('sb-required-actions-failure');
const sbRequiredActionsFailureSubtext = document.getElementById('sb-required-actions-failure-subtext');
-
const sbRequiredActionsList = document.getElementById('sb-required-actions-list');
const sbImportCodeLabel = document.getElementById('sb-import-code');
const sbImportCopyButton = document.getElementById('sb-import-copy-button');
-
const sbActionsButton = document.getElementById('sb-actions-button');
const sbStatusButton = document.getElementById('sb-status-button');
const sbStatusIcon = document.getElementById('sb-status-icon');
const blurLayer = document.getElementById('blur-layer');
+const contentIFrame = document.getElementById('content');
+
+
+//////////////////////
+// GLOBAL VARIABLES //
+//////////////////////
+
+let sbClientListeners;
+
/////////////////////////
@@ -54,7 +62,7 @@ const sbServerAddress = sbAddressInput.value;
const sbServerPort = sbPortInput.value;
const sbServerPassword = sbPasswordInput.value;
-window.sbClient = new StreamerbotClient({
+sbClient = new StreamerbotClient({
host: sbServerAddress,
port: sbServerPort,
password: sbServerPassword,
@@ -65,6 +73,16 @@ window.sbClient = new StreamerbotClient({
console.debug(data);
SetConnectionState(true);
+
+ // Notify iframe
+ contentIFrame.addEventListener("load", () => {
+ NotifyTheContentIFrameThatSBHasConnectedSuccessfullySoItCanDoStuff(data);
+ });
+ NotifyTheContentIFrameThatSBHasConnectedSuccessfullySoItCanDoStuff();
+
+ // Idk why but listeners get cleared when re-connecting, so copy them back in
+ if (sbClientListeners)
+ sbClient.listeners = sbClientListeners;
},
onDisconnect: () => {
@@ -89,7 +107,7 @@ function SetConnectionState(isConnected) {
sbErrorLabel.style.display = 'none';
sbStatusIcon.src = 'icons/connected.svg';
- sbStatusButton.title = `Connected to ${window.sbClient.info.name} (${window.sbClient.info.version})`;
+ sbStatusButton.title = `Connected to ${sbClient.info.name} (${sbClient.info.version})`;
// Check required actions
CheckRequiredActions();
@@ -157,7 +175,7 @@ async function CheckRequiredActions() {
// Iterate over required SB actions and check if they're present
config.requiredSbActions.forEach(req => {
const exists = response.actions.some(act => act.id === req.id);
-
+
console.debug(`${req.name}: ${exists ? 'Found' : 'Missing'}`)
// As soon as one is found that doesn't exist, throw up warning
@@ -188,8 +206,7 @@ async function CheckRequiredActions() {
sbRequiredActionsList.appendChild(item);
});
}
- else
- {
+ else {
// There are no required actions, so hide the button
sbActionsButton.style.display = 'none';
}
@@ -199,8 +216,7 @@ async function CheckRequiredActions() {
}
function SetRequiredActionState(isSuccess) {
- if (isSuccess)
- {
+ if (isSuccess) {
sbRequiredActionsSuccessLabel.style.display = 'block';
sbRequiredActionsFailureLabel.style.display = 'none';
sbRequiredActionsFailureSubtext.style.display = 'none';
@@ -209,12 +225,11 @@ function SetRequiredActionState(isSuccess) {
sbActionsButton.textContent = '✅';
}
- else
- {
+ else {
sbRequiredActionsSuccessLabel.style.display = 'none';
sbRequiredActionsFailureLabel.style.display = 'block';
sbRequiredActionsFailureSubtext.style.display = 'block';
-
+
sbActionsButton.title = `You are missing Streamer.bot actions`;
sbActionsButton.textContent = '⚠️';
@@ -228,7 +243,8 @@ function SetRequiredActionState(isSuccess) {
// PAGE INTERACTIONS //
///////////////////////
-function Connect() {
+function Connect() {
+ sbClientListeners = sbClient.listeners;
sbClient.options.host = sbAddressInput.value;
sbClient.options.port = sbPortInput.value;
sbClient.options.password = sbPasswordInput.value;
@@ -240,7 +256,7 @@ function CopyImportCode() {
navigator.clipboard.writeText(textToCopy)
.then(() => {
console.debug('Copied to clipboard!');
-
+
// Click feedback
const root = document.documentElement;
const successColor = getComputedStyle(root).getPropertyValue('--success-color').trim();
@@ -273,4 +289,11 @@ function OpenRequiredActionsDialog() {
function CloseRequiredActionsDialog() {
sbRequiredActionsDialog.style.display = "none";
blurLayer.style.display = "none";
+}
+
+function NotifyTheContentIFrameThatSBHasConnectedSuccessfullySoItCanDoStuff(data) {
+ contentIFrame.contentWindow.postMessage(
+ { type: "sbClientConnected", data },
+ "*" // replace with iframe origin for security if needed
+ );
}
\ No newline at end of file
diff --git a/.common/core/widget-dock-core/style.css b/.common/core/widget-dock-core/style.css
index 53dca40..198d01e 100644
--- a/.common/core/widget-dock-core/style.css
+++ b/.common/core/widget-dock-core/style.css
@@ -17,7 +17,8 @@ body {
align-items: center;
padding: 0em 1em;
gap: 1em;
- box-shadow: 0px 0px 10px rgba(0, 0, 0, 1);
+ box-shadow: 0 0 10px rgba(0,0,0,1);
+ z-index: 1;
}
#header-end {
@@ -25,11 +26,6 @@ body {
align-items: center;
display: flex;
flex-direction: row;
- gap: 1em;
-}
-
-#header-end button {
- padding: 0.2em;
}
@@ -41,21 +37,6 @@ body {
#content {
flex: 1;
overflow: auto;
- margin: 1em;
-}
-
-
-
-/******************/
-/*** BLUR LAYER ***/
-/******************/
-
-#blur-layer {
- position: fixed;
- inset: 0; /* top:0; left:0; bottom:0; right:0 */
- backdrop-filter: blur(10px); /* blur the content behind */
- -webkit-backdrop-filter: blur(10px); /* Safari support */
- z-index: 1; /* behind the modal */
}
diff --git a/.common/styles/global.css b/.common/styles/global.css
index ef0826b..c532de9 100644
--- a/.common/styles/global.css
+++ b/.common/styles/global.css
@@ -34,20 +34,25 @@ body {
/************/
.title {
- font-weight: 900;
- font-size: 1.2em;
- text-transform: uppercase;
+ font-weight: 900;
+ font-size: 1.2em;
+ text-transform: uppercase;
}
.field {
- display: flex;
- flex-direction: column;
- gap: 0.5em;
+ display: flex;
+ flex-direction: column;
+ gap: 0.5em;
}
.setting-description {
- font-size: 0.9em;
- font-weight: 100;
+ font-size: 0.9em;
+ font-weight: 200;
+}
+
+.setting-attribute {
+ font-size: 0.8em;
+ font-weight: 200;
}
@@ -80,6 +85,9 @@ button {
padding: 0.5em 1em;
width: 100%;
transition: all 0.2s ease-in-out;
+ display: flex;
+ align-items: center;
+ justify-content: center;
}
button:hover {
@@ -92,26 +100,66 @@ button:disabled {
cursor: inherit;
}
+.icon-button {
+ margin-left: auto;
+ width: 1em;
+ height: 1em;
+
+ background: transparent;
+ border: none;
+ font-size: 1.5em;
+ font-weight: 100;
+ padding: 1em;
+ border-radius: 50%;
+}
+
+.icon-button:hover {
+ background: var(--button-color);
+}
+
/************************/
/*** INPUT TEXT BOXES ***/
/************************/
+textarea,
input {
+ font-family: inherit;
border-radius: 0.5em;
width: auto;
padding: 0.5em;
background-color: #171717;
- border: 1px solid #404040;
+ border: none;
+ outline: 1px solid #404040;
color: white;
font-size: 0.9em;
+ transition: 0.2s;
}
+textarea:disabled,
input:disabled {
opacity: 0.5;
}
+textarea:focus,
+input:focus,
+select:focus {
+ outline: 1px solid var(--accent-color);
+}
+
+textarea {
+ resize: vertical;
+}
+
+.textarea-description {
+ min-height: 10em;
+}
+
+textarea::-webkit-resizer {
+ display: none;
+}
+
/***********************/
@@ -226,16 +274,16 @@ iframe {
/***************/
.callout {
- font-size: 0.9em;
- font-weight: 100;
- background-color: var(--callout-background);
- display: flex;
- flex-direction: column;
- gap: 1em;
- padding: 1em;
+ font-size: 0.9em;
+ font-weight: 100;
+ background-color: var(--callout-background);
+ display: flex;
+ flex-direction: column;
+ gap: 1em;
+ padding: 1em;
- border-radius: 0.5em;
- border: 1px solid #40404080;
+ border-radius: 0.5em;
+ border: 1px solid #40404080;
}
@@ -245,21 +293,22 @@ iframe {
/**************/
.dialog {
- font-size: 1em;
- background-color: var(--dialog-background);
- position: absolute;
- left: 50%;
- top: 50%;
- transform: translate(-50%, -50%);
- display: flex;
- flex-direction: column;
- gap: 1em;
- padding: 1em;
+ font-size: 1em;
+ background-color: var(--dialog-background);
+ position: absolute;
+ left: 50%;
+ top: 50%;
+ transform: translate(-50%, -50%);
+ display: flex;
+ flex-direction: column;
+ gap: 1em;
+ padding: 1em;
- border-radius: 0.5em;
- border: 1px solid #40404080;
- box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5);
- z-index: 1000; /* above overlay */
+ border-radius: 0.5em;
+ border: 1px solid #40404080;
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5);
+ z-index: 1000;
+ /* above overlay */
}
.dialog-nav-button {
@@ -271,11 +320,49 @@ iframe {
width: 1em;
height: 1em;
position: absolute;
- position: fixed; /* fixed to viewport */
- top: 0.5em; /* small offset from top */
- right: 0.5em; /* small offset from right */
+ position: fixed;
+ /* fixed to viewport */
+ top: 0.5em;
+ /* small offset from top */
+ right: 0.5em;
+ /* small offset from right */
}
.dialog-nav-button:hover {
background: var(--button-color);
+}
+
+
+
+/******************/
+/*** BLUR LAYER ***/
+/******************/
+
+.blur {
+ position: fixed;
+ inset: 0;
+ /* top:0; left:0; bottom:0; right:0 */
+ backdrop-filter: blur(10px);
+ /* blur the content behind */
+ -webkit-backdrop-filter: blur(10px);
+ /* Safari support */
+ z-index: 1;
+ /* behind the modal */
+}
+
+
+/***************/
+/*** SELECTS ***/
+/***************/
+
+select {
+ padding: 0.5em;
+ font-size: 0.9em;
+ border-radius: 0.5em;
+ border: none;
+ outline: 1px solid #404040;
+ background-color: var(--callout-background);
+ color: #fff;
+ cursor: pointer;
+ min-width: 8em;
}
\ No newline at end of file
diff --git a/multistream-title-updater/connected.svg b/.old/multistream-title-updater/connected.svg
similarity index 100%
rename from multistream-title-updater/connected.svg
rename to .old/multistream-title-updater/connected.svg
diff --git a/multistream-title-updater/disconnected.svg b/.old/multistream-title-updater/disconnected.svg
similarity index 100%
rename from multistream-title-updater/disconnected.svg
rename to .old/multistream-title-updater/disconnected.svg
diff --git a/multistream-title-updater/icons.afdesign b/.old/multistream-title-updater/icons.afdesign
similarity index 100%
rename from multistream-title-updater/icons.afdesign
rename to .old/multistream-title-updater/icons.afdesign
diff --git a/.old/multistream-title-updater/index.html b/.old/multistream-title-updater/index.html
new file mode 100644
index 0000000..4c088ec
--- /dev/null
+++ b/.old/multistream-title-updater/index.html
@@ -0,0 +1,81 @@
+
+
+
+
+
+
+
+
+
+
+
Refreshed
+
+
+
+
+ Please connect to Streamer.bot.
+
+ Click
here for instructions.
+
+
+
+
+ Streamer.bot is connected, but you are missing actions.
+ Please import
and refresh the page.
+ Click
here for instructions.
+
+
+
+
+
+
+
+
+
Update Titles
+
+

+
+
+
+
+
+
+
+
+
+ ⚠️ YouTube titles can only be updated after the stream has started ⚠️
+
+ Please start your stream to update YouTube titles
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/multistream-title-updater/info.svg b/.old/multistream-title-updater/info.svg
similarity index 100%
rename from multistream-title-updater/info.svg
rename to .old/multistream-title-updater/info.svg
diff --git a/multistream-title-updater/requiredActions.txt b/.old/multistream-title-updater/requiredActions.txt
similarity index 98%
rename from multistream-title-updater/requiredActions.txt
rename to .old/multistream-title-updater/requiredActions.txt
index 503a772..c43224d 100644
--- a/multistream-title-updater/requiredActions.txt
+++ b/.old/multistream-title-updater/requiredActions.txt
@@ -1,4 +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 | Fetch Broadcasts
+[NUT] Multistream Title Updater | Update All Broadcasts
+[NUT] Multistream Title Updater | Update Twitch Title
[NUT] Multistream Title Updater | Update YouTube Title
\ No newline at end of file
diff --git a/.old/multistream-title-updater/script.js b/.old/multistream-title-updater/script.js
new file mode 100644
index 0000000..72ec198
--- /dev/null
+++ b/.old/multistream-title-updater/script.js
@@ -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();
\ No newline at end of file
diff --git a/multistream-title-updater/style.css b/.old/multistream-title-updater/style.css
similarity index 100%
rename from multistream-title-updater/style.css
rename to .old/multistream-title-updater/style.css
diff --git a/multistream-title-updater/twitch.png b/.old/multistream-title-updater/twitch.png
similarity index 100%
rename from multistream-title-updater/twitch.png
rename to .old/multistream-title-updater/twitch.png
diff --git a/multistream-title-updater/youtube.png b/.old/multistream-title-updater/youtube.png
similarity index 100%
rename from multistream-title-updater/youtube.png
rename to .old/multistream-title-updater/youtube.png
diff --git a/.templates/obs-dock/config.json b/.templates/obs-dock/config.json
new file mode 100644
index 0000000..32ca26f
--- /dev/null
+++ b/.templates/obs-dock/config.json
@@ -0,0 +1,6 @@
+{
+ "title": "Widget",
+ "requiredSbActions": [
+ ],
+ "sbImportCode": "U0JBRR+LCAAAAAAABADtW9ty4ki2fZ+I+QdHndeWIzN1QZqI82AwCGFMF8Ig0Lgf8iIJFZKgQYDxTP/72SkJbAzY1RNn3F0T4yiXjXZe9nXtlYn5x1//cnX1JQ1y+uVvV/+QL+BlRtMAXn75uoyzPFhe1ef5l58qGV3n0/lSSrN1nu8OzzfBchXPMynA1/gaHQQiWPFlvMgr4euF5u46u+GVJFsnyV6WxlmcrtPRYU0plLLfihFfBD3SlxZrrODJ38snV3tRIY6F3JgEGmKEEYXWdKxoOtIUhhlVqEFM1cBU03i4V66Y9us6WBduQNWXcua//dfRzCCjLAnkrvlyHRxJnniyFkFrOU/b8SqfL3cwKKTJ6tKor0Em4iw6N+pMlK7+edXcBFm+OlInWs7Xi0PErqPoSEqTLd2tIBLn9ljSTMzTQ4xO5Hye8fVyCVuek+bLOIoghq8D8yY4+3DDEIzQT28F9AkECn77vIyogVCIVI6UwDQpRLRGFUsz4LdAEIQ1YhDd+vJ2ar5bBMVm5K3kYtReYrLap9kvr6W//fSecXlcegAb561QWU0liNcUbBiWoolaTTE1RBSNqUYtJGrILHrZCvUPt6IM3mmM3o+dhrBhUWQqIhShotW4UKjQsMIZskyhUpWE+LLV2g9n9WrNHkrtT5N8nuXTlR2HeWEA1s97TKiasHTGwE8YPEYwUSxu6AqluopEGNLQesdjJ4v+yB4rHVIzQ0sNCVEIYYGiYYtB4YBDDI1olBhYM3XzskNO9Ps3OaQ0+8T971eHCiYYoeCAZ2FN0TSmKdQ0iRKoODTCUDNNi102rfZJppW6WoamQekKxSQahAGpumIJqGkjCJBaMzVdZeElXTWEzE+NgwLM5FIoTiWVgTqrYU23lJpRM8FAyRlQGCg6NQnYZ2BT5e8Y+FmJVqGEGYga1XVFpTVgOYJwKIoAKZQFIcKUcTUwLit7ij1/umioFlIDaglFmEgaqJqKyQRWiI4CZmosZGHtnWh8bm1oBtEwQLNS06DVaUFNUyzV0gG9BQ94aOqopl5SVtWBw3yqtiHBvGZh8CrWAXVMXdY04KuFQgOpDBDpfW0/K3mqshTc0IBEKKbOgDcRThVARXjJVG5CWwgBk97T9nPr0gprQhWEKkQjkquGSGEMWrigcPBQdZ0jA72n7Wfl7b9emEhnguAAeKwmGR0xDMh16AgGN7BQkUks9WJhYuDrf3oDMVE1avFACSBkQFk1Ca1QI4Guq9TQAvh3kYD9CPYx6FWGEJqCORxuoZFD5TOtBm0Pa2CysLB+sc+RT290FrewpatcqQVSWx4aikkp/IdVlVKOKOYXG53U9nOhFZs18CDBSojgfAe5g4FD8FDBNRLwgGmhjsl72n5W+e/vFP5+F/PZNZvnv1y5ND7F0UDeMvSqwTMY62R8DqkXnR1d+QAMhVQClm6oQBSNUFUoVg1FRcQMTWwKoesXAcJE/58H9pcXv7y+r4Ajx83pVdI5P21oUt4NNeYiOFGaz5Pyiux/TGSiVuuCQwhRCVfh7BKYhryRCgBQzECegQNMgYzRcx1sG8TRVN62oNM6PpwAkPVWtKDyksaR+766aPtOV8aZCCR2oN+fSCf6X7gOPMiXQRiAqjw4iUEhbvzt8dEDfebb1ePjfcyX89U8zK97zYfHx9YSNt3OlzNDe3zcaNfoWkUqth4f0xWfL5OYXYskebvhv7rmYLfKg7RY8XjBX95axHZ5UKSJpOfj3oKlPBqqybOwR/nPW3S3f/aQjlRhW2tOrFQ09Dv4uf5I3p09LVjWrN32571GVseT9Gkx2dW/Mbv1zHf122Fz2mHwjKVDkK96jSjZCq+zot59NEmtDWvUW4E9+ibGbnLXmEm5XKvTGC6+TtJFMlH7kW+30GRwYznNw7P10G7tOBnOB1lvw2bwnfkJz+A5Ge2o11rR8SJ5IJ1ffa+H7mCsr94vuom7GaouyPUM9lpI2xr9ZDYZu1Nv1wnHam8jxp1v/qAjnFsU9XHd6c5EIpoj4ns6GrWTrT+I3q5T6dvrDxr62Pc6z4z0lv7YbfDUmvp2b8pVt/DVne1Ohd1cj2zrQbQ70qfzu0E1v1/osoN5mKdaBHLsPxT+sm77i9NrxMUyAMBbxElw5uKzSvSE7gY5XZ67Gi1GrOgmcIPVOskf5iO6jGUJvjf2aNRp5ZSgQg1MsIUMRfJK6IuGqVCVBIpFBNJqwIwN9bTTfAeoWPLr34Iq+HugGSA1oYtVIGx5mX0M6S9IdHrrr/OabuhYthoBNIGY8naCcYUAuqqaZmEVH9/d/5i3/sWrK3e+zuPsqB/9OS7//8guW+OYchWHiq7rkAGhThQayGuR0ERcR8AUySlR/G+X/U/tsvtn3ZmeiFtA+Gz0LDuJ0+rp0CkSNtBvGXlayc5FPei2O70N3SfhafLt93Trl32Sr+efu4uJJ9ZMlfv0ns+OSTqJnz5Bx3UXjGjQqRaYk2Tt7+oPwbiHfA+th9loLewk9wd6h2Uu6GltZRcWoK8/vj87Z2SPtAu2D2m7k0w8V+rxe1jFc9VRgVW0nv1RHTptFH0d3MDBIK/zdr3JCJ5ST1v31dGOp6Bz090wr7Vgsd6YjHuJa0P3BZ8ztaOHno5F29W7qcjGract+B5sHO26XmcjGk7RsXnFABz7aeNjd+rvgKE0Omwo9ZB+iOsNprrC+Sb1g+4+PthBGElmTjNZ03F/fic7/1iOmXUKxiF9Tgr/70BX+GnlI1jTJwLWjOJy/X40SK0Y/JbA73G3cROzbIREOwlhXMj3OiRWDDrsX4cU5gDLqebU1/5YsPGgXnPSF3udeBs5SV3ahvyxE/VTCzkZKtnSGEk9v5/FDcyN03zaTLx+BDmBIE/WwMRe2erugqFkWMDQdlEMcfnme7B2czp8GG6ljsDI+tHB1zI3yGjB264zauL7u4fV5bXs1tYfd8bUcxFtOCvHtrBo1AvfHcuOGNyQZ0kbWFidAztzsr0fOj9PPJxI3zB1hJx2D8mcgJw+5MZD2oIaeGetNMlYau38EcRFdYcwL/OhTvjuoFvMiAW/Xxr33tpQO/YIyTlsXLep95ScrntuzLv6PkOdPovGiX7V8yPmeqg/P00Au/Qp84ZQf/W28JJZgXVIYOpBvkONM7z3X7Rn4Yf5End4Gf99/cayPoHxI9rGfNwu9IefeSptYNLvY7SmbTdnVW02xof4AN7g7bDp2pLRdxqd2gs+TC2o7UIOP2sveo+sblaf+bFT4g/kV3dnRn0Vcg/y27GbpY62tZuMO7pT6VrML31xC3kM64/KHH2Ts5DTxZ6Dltt7OPLhbNMFbCvnDqPJoBwHeDTl2SwqbLD9BbOHB51cqDkYn8vTidOuTznk4QiwRWLfBOb8DHuxXX06IXCykWNsfwO4tJ2M6xKfABs6M1njVQ4A/rUgP+pNN56+0nFb2tuSz7dvaucmd273MZwd9IL82DEvWZf+SlL4Rh31vrQJYlPs5yXp3SGXbtJOXAeM0zdgz1COOWBedr8ofXTudHTwW7+oQ6hv0WgWelQ43Hebk/mR/ntsnBU5unLKfNrHZuZDTsH4obTflTEd34N9s9d7PYC/Z47dw1CfGwY27Wv0qJaqZ31izfxheYK8a3TWgJEoGFmZj60DdowJ9JVW9bpl/cpIJ+QEepSsvba7E97wyMfgIwQn1XUlK3IohBwq+lJ7FDM7+ea0/Q3U2yFvy1hOD3X2BmtXrzDtdX5GQVmf0HOj9YNtZQNPT9kuknW5YkQMgUPAybzMnRKjpT9vomL+7TwK1OP8DwfOvuYLDOomYkdBv773tCp4QeM4997o+aoPneupRzl8sLXAhqasnRHkvjuHOB9kEs/BZ6iMdcEbJMbJXCrrJBELv+0e9em3eAA5tQEbJTeR9batchFRqGNuu2mRF6P6jpEeYPr9q/HAGWzAxiJ/LeBLvefqRqGM82Gc/iDs1k4AZ/BTc13oOtxj7M38DK5VOmAkMZvjJy5IDnWAV8Antr49gd4/hX5W4c/4oNt6qLqANy7wK38DsQEfuRsBeB1ArCSGDvc64WNZdzZd+LLHfCt99AaHS32axU2M1Dl2qnFVPULPEc9OYwUcCrjJAG8hr5LuuPS9086PYhF6OL8834kO871pQj0xB3zB97f1nA2cg72yr/fHnQxisCz0aaDcBzyaAK/xB/gZuOEO/PdMizzJZa7EVd4e2XL3jCInvsm7XsFzoBd1dkx1cmq31r79VOZfwbM7yQc6r9kOxxOvt/TVjsTr2QfjZe+DWh/mNG0R6JHQC6BOGh/YCD0OaiYp9CWtVdcrekoO+xb598Gev07G/pSTHtRsHXU9ySlaOvSA7f0H+0p/Aq9dd8fuhjduth/sk0NvzYCv58AvkeTAzu3NB/7HU1gD+DyGPu6j3zF+V+XIOzr1gWtv44BMEfOeSu5U8REnvmj3zInrHHASOKy7he9z81746wzOEaio76KeGWDtyG5l4aDqbXZRR7KfGVAPZ9ZE8d1xTxwID3qlDXXefsEeYZvRBM6KIoUzWqNeYAJg9gbGzPxo768Si7pJD0EcUIn5uvQHBn60Fm1Zw0c1/QGOwJmmOl/J54Ab4Hv3a2lDX66FeDZKXuJ1dv+BD+dC2Q9EC55DfYBs5pb19r1rjKCvPlB7tGLNkQb6YMm1YG468Z6e/YfVu7i7x/yH1AL+o8+Yyj/c9+6YJ7+Siyn0PRtwAc5OCTTv4fb+9ubiOUuk1sJvVLnQOu5v/uvedfFssn05R9lv8hj6bHWOO8t/pP4lDzjhqAd7qhwqagB+n/sggz6z52L7/vgyHnoJS0eq0zrkzHx/nns3l2ZFHy/PY7AGKAV1CRyZjPqyt4ejJ17Wbz8H3Niwdg/OCj3I2xb4pXXa+96cX45q/1ZixXv1jV/67O5DHDiOx/gj7LjJ93bwl7NzNAYOydTeN8jXZNxwLmJcB3AUxtZecgN4wQfju/L+xy5450Vseu2f/+LNfwjevDq79clTMpFxHN8Uz6g3ie7O3x0U8goXOgVWDPRmiR/D+StM2u9xMuYtL9/Xf9ivMDBD//v+O1/n7rH/oDe+TE03TFMNFEFCoWg1LBTGNUthRoiQUC0Vhad/XvCDv/FV/rIfX753dfSGC0xPU5qJ44fbgK3mfBbkg2C5efMWzYuwkcTFR0leC/M43Y+XT6qP47x89oeUn0f4Ejwt5ss8EPLNrOJvP67RdfXHUacf7imkSKHJYkph1F//8tv/AVT//+2WNAAA"
+}
\ No newline at end of file
diff --git a/.templates/obs-dock/contents/index.html b/.templates/obs-dock/contents/index.html
new file mode 100644
index 0000000..6b37cd3
--- /dev/null
+++ b/.templates/obs-dock/contents/index.html
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.templates/obs-dock/contents/script.js b/.templates/obs-dock/contents/script.js
new file mode 100644
index 0000000..9eb8555
--- /dev/null
+++ b/.templates/obs-dock/contents/script.js
@@ -0,0 +1,45 @@
+//////////////////////
+// GLOBAL VARIABLES //
+//////////////////////
+
+const sbAction = '';
+
+
+
+/////////////////////////
+// STREAMER.BOT EVENTS //
+/////////////////////////
+
+window.parent.sbClient.on('General.Custom', (response) => {
+ console.debug(response.data);
+ CustomEvent(response.data);
+})
+
+
+
+/////////////////
+// PRINTER BOT //
+/////////////////
+
+
+
+
+//////////////////////
+// HELPER FUNCTIONS //
+//////////////////////
+
+
+
+
+
+///////////////////////
+// PAGE INTERACTIONS //
+///////////////////////
+
+
+
+
+
+///////////////////
+// PAGE SETTINGS //
+///////////////////
\ No newline at end of file
diff --git a/.templates/obs-dock/contents/style.css b/.templates/obs-dock/contents/style.css
new file mode 100644
index 0000000..9fa4a22
--- /dev/null
+++ b/.templates/obs-dock/contents/style.css
@@ -0,0 +1,3 @@
+body {
+ margin: 1em;
+}
\ No newline at end of file
diff --git a/.templates/obs-dock/index.html b/.templates/obs-dock/index.html
new file mode 100644
index 0000000..332b16c
--- /dev/null
+++ b/.templates/obs-dock/index.html
@@ -0,0 +1,20 @@
+
+
+
+
+
+
nutty
+
+
+
+
+
+
+
+
+