|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
@@ -0,0 +1,88 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<link rel="shortcut icon" href="#" />
|
||||||
|
<link rel="stylesheet" href="../../styles/global.css" />
|
||||||
|
<link rel="stylesheet" href="./style.css" />
|
||||||
|
<script type="text/javascript" src="https://unpkg.com/@streamerbot/client/dist/streamerbot-client.js"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/luxon@3/build/global/luxon.min.js"></script>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<!-- Header -->
|
||||||
|
<div id="header">
|
||||||
|
<img src="../../resources/logo.png" style="height: 40px;" />
|
||||||
|
<label id="title" class="title"></label>
|
||||||
|
<div id="header-end">
|
||||||
|
<button id="sb-actions-button" class="icon-button" onclick="OpenRequiredActionsDialog()">
|
||||||
|
</button>
|
||||||
|
<button id="sb-status-button" class="icon-button" onclick="OpenConnectDialog()">
|
||||||
|
<img id="sb-status-icon" style="height: 1em;" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Page Contents -->
|
||||||
|
<iframe id="content">
|
||||||
|
</iframe>
|
||||||
|
|
||||||
|
<!-- Blur Layer -->
|
||||||
|
<div id="blur-layer" class="blur"></div>
|
||||||
|
|
||||||
|
<!-- Streamer.bot Required Actions Dialog -->
|
||||||
|
<div id="sb-required-actions-dialog" class="dialog">
|
||||||
|
|
||||||
|
<button class="dialog-nav-button" onclick="CloseRequiredActionsDialog()">×</button>
|
||||||
|
|
||||||
|
<label id="sb-required-actions-success">✅ All actions found</label>
|
||||||
|
|
||||||
|
<label id="sb-required-actions-failure"><span>⚠️</span> You are missing Streamer.bot actions</label>
|
||||||
|
<label id="sb-required-actions-failure-subtext" class="setting-description">The following actions were not found in Streamer.bot:</label>
|
||||||
|
|
||||||
|
<div id="sb-required-actions-list" class="callout">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<label class="setting-description">Please import the following </label>
|
||||||
|
|
||||||
|
<div id="sb-import-wrapper">
|
||||||
|
<label id="sb-import-code" class="callout"></label>
|
||||||
|
<button id="sb-import-copy-button" onclick="CopyImportCode()">Copy</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button onclick="CheckRequiredActions()">Check Again</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Streamer.bot Connect Dialog -->
|
||||||
|
<div id="sb-connect-dialog" class="dialog">
|
||||||
|
|
||||||
|
<button class="dialog-nav-button" onclick="ClosenConnectDialog()">×</button>
|
||||||
|
|
||||||
|
<div style="display: flex; flex-direction: row; align-items: center; gap: 1em">
|
||||||
|
<img src="../../resources/streamer.bot.png" style="height: 40px;" />
|
||||||
|
<label class="title">Streamer.bot</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="display: flex; flex-direction: row; gap: 1em">
|
||||||
|
<div class="field">
|
||||||
|
<label for="sb-address">Address <span style="color: var(--mandatory-field-color);">*</span></label>
|
||||||
|
<input type="text" id="sb-address" name="sb-address" placeholder="127.0.0.1" value="127.0.0.1">
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label for="sb-port">Port <span style="color: var(--mandatory-field-color);">*</span></label>
|
||||||
|
<input type="text" id="sb-port" name="sb-port" placeholder="8080" value="8080">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label for="sb-password">Password</label>
|
||||||
|
<input type="password" id="sb-password" name="sb-password">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<label id="sb-error-label"></label>
|
||||||
|
|
||||||
|
<button id="sb-connect-button" onclick="Connect()">Connect</button>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
<script src="./script.js"></script>
|
||||||
@@ -0,0 +1,299 @@
|
|||||||
|
////////////////
|
||||||
|
// PARAMETERS //
|
||||||
|
////////////////
|
||||||
|
|
||||||
|
const queryString = window.location.search;
|
||||||
|
const urlParams = new URLSearchParams(queryString);
|
||||||
|
|
||||||
|
const configJson = urlParams.get("config") || "";
|
||||||
|
|
||||||
|
|
||||||
|
///////////////////
|
||||||
|
// PAGE ELEMENTS //
|
||||||
|
///////////////////
|
||||||
|
|
||||||
|
const sbConnectDialog = document.getElementById('sb-connect-dialog');
|
||||||
|
const sbAddressInput = document.getElementById('sb-address');
|
||||||
|
const sbPortInput = document.getElementById('sb-port');
|
||||||
|
const sbPasswordInput = document.getElementById('sb-password');
|
||||||
|
const sbErrorLabel = document.getElementById('sb-error-label');
|
||||||
|
|
||||||
|
const sbRequiredActionsDialog = document.getElementById('sb-required-actions-dialog');
|
||||||
|
const sbRequiredActionsSuccessLabel = document.getElementById('sb-required-actions-success');
|
||||||
|
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;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/////////////////////////
|
||||||
|
// STREAMER.BOT CLIENT //
|
||||||
|
/////////////////////////
|
||||||
|
|
||||||
|
// Check local storage
|
||||||
|
if (localStorage.getItem('sbServerAddress') === null)
|
||||||
|
localStorage.setItem('sbServerAddress', '127.0.0.1');
|
||||||
|
if (localStorage.getItem('sbServerPort') === null)
|
||||||
|
localStorage.setItem('sbServerPort', '8080');
|
||||||
|
|
||||||
|
sbAddressInput.value = localStorage.getItem('sbServerAddress');
|
||||||
|
sbPortInput.value = localStorage.getItem('sbServerPort');
|
||||||
|
sbPasswordInput.value = localStorage.getItem('sbServerPassword');
|
||||||
|
|
||||||
|
const sbServerAddress = sbAddressInput.value;
|
||||||
|
const sbServerPort = sbPortInput.value;
|
||||||
|
const sbServerPassword = sbPasswordInput.value;
|
||||||
|
|
||||||
|
sbClient = new StreamerbotClient({
|
||||||
|
host: sbServerAddress,
|
||||||
|
port: sbServerPort,
|
||||||
|
password: sbServerPassword,
|
||||||
|
immediate: true,
|
||||||
|
|
||||||
|
onConnect: (data) => {
|
||||||
|
console.log(`Streamer.bot successfully connected to ${sbServerAddress}:${sbServerPort}`)
|
||||||
|
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: () => {
|
||||||
|
console.error(`Streamer.bot disconnected from ${sbServerAddress}:${sbServerPort}`)
|
||||||
|
SetConnectionState(false);
|
||||||
|
},
|
||||||
|
|
||||||
|
onError: (err) => {
|
||||||
|
console.error(`Streamer.bot disconnected from ${sbServerAddress}:${sbServerPort}`)
|
||||||
|
SetErrorMessage(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function SetConnectionState(isConnected) {
|
||||||
|
if (isConnected) {
|
||||||
|
localStorage.setItem('sbServerAddress', sbAddressInput.value);
|
||||||
|
localStorage.setItem('sbServerPort', sbPortInput.value);
|
||||||
|
localStorage.setItem('sbServerPassword', sbPasswordInput.value);
|
||||||
|
|
||||||
|
sbConnectDialog.style.display = "none";
|
||||||
|
blurLayer.style.display = "none";
|
||||||
|
sbErrorLabel.style.display = 'none';
|
||||||
|
sbStatusIcon.src = 'icons/connected.svg';
|
||||||
|
|
||||||
|
sbStatusButton.title = `Connected to ${sbClient.info.name} (${sbClient.info.version})`;
|
||||||
|
|
||||||
|
// Check required actions
|
||||||
|
CheckRequiredActions();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
sbConnectDialog.style.display = "flex";
|
||||||
|
blurLayer.style.display = "block";
|
||||||
|
sbStatusIcon.src = 'icons/disconnected.svg';
|
||||||
|
|
||||||
|
SetErrorMessage('Disconnected from Streamer.bot');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function SetErrorMessage(error) {
|
||||||
|
sbErrorLabel.textContent = error;
|
||||||
|
sbErrorLabel.style.display = 'block';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
////////////
|
||||||
|
// CONFIG //
|
||||||
|
////////////
|
||||||
|
|
||||||
|
if (configJson) {
|
||||||
|
// Set the header/title of the page
|
||||||
|
fetch(configJson)
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(config => {
|
||||||
|
const root = document.documentElement;
|
||||||
|
const domain = getComputedStyle(root).getPropertyValue('--domain').trim();
|
||||||
|
|
||||||
|
// Set the page title
|
||||||
|
parent.document.title = `${domain} • ${config.title}`;
|
||||||
|
|
||||||
|
// Set the title label
|
||||||
|
title.textContent = config.title;
|
||||||
|
})
|
||||||
|
.catch(err => console.error('Failed to load config:', err));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function CheckRequiredActions() {
|
||||||
|
if (configJson) {
|
||||||
|
console.debug('Checking required actions...')
|
||||||
|
|
||||||
|
// Set the header/title of the page
|
||||||
|
fetch(configJson)
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(async config => {
|
||||||
|
// Clear the required actions list
|
||||||
|
const sbRequiredActionsList = document.getElementById('sb-required-actions-list');
|
||||||
|
sbRequiredActionsList.innerHTML = '';
|
||||||
|
|
||||||
|
// Set the import code
|
||||||
|
sbImportCodeLabel.textContent = config.sbImportCode;
|
||||||
|
|
||||||
|
// Assume all actions are found
|
||||||
|
SetRequiredActionState(true);
|
||||||
|
|
||||||
|
// Check each required action
|
||||||
|
if (config.requiredSbActions) {
|
||||||
|
// Get a full list of actions currently installed in Streamer.bot
|
||||||
|
const response = await sbClient.getActions();
|
||||||
|
|
||||||
|
// 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
|
||||||
|
if (!exists)
|
||||||
|
SetRequiredActionState(false);
|
||||||
|
|
||||||
|
// container div
|
||||||
|
const item = document.createElement('div');
|
||||||
|
item.className = 'sb-required-action';
|
||||||
|
|
||||||
|
// name label
|
||||||
|
const nameLabel = document.createElement('label');
|
||||||
|
nameLabel.textContent = req.name;
|
||||||
|
|
||||||
|
// status label
|
||||||
|
const statusLabel = document.createElement('label');
|
||||||
|
statusLabel.className = 'sb-required-action-found';
|
||||||
|
//statusLabel.textContent = exists ? '✅' : '❌';
|
||||||
|
statusLabel.textContent = exists ? 'Found' : 'Missing';
|
||||||
|
statusLabel.style.color = exists ? '#00d26a' : '#f92f60';
|
||||||
|
statusLabel.style.fontWeight = 500;
|
||||||
|
|
||||||
|
// append labels to div
|
||||||
|
item.appendChild(nameLabel);
|
||||||
|
item.appendChild(statusLabel);
|
||||||
|
|
||||||
|
// append div to list
|
||||||
|
sbRequiredActionsList.appendChild(item);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// There are no required actions, so hide the button
|
||||||
|
sbActionsButton.style.display = 'none';
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => console.error('Failed to load config:', err));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function SetRequiredActionState(isSuccess) {
|
||||||
|
if (isSuccess) {
|
||||||
|
sbRequiredActionsSuccessLabel.style.display = 'block';
|
||||||
|
sbRequiredActionsFailureLabel.style.display = 'none';
|
||||||
|
sbRequiredActionsFailureSubtext.style.display = 'none';
|
||||||
|
|
||||||
|
sbActionsButton.title = `All actions found`;
|
||||||
|
|
||||||
|
sbActionsButton.textContent = '✅';
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
sbRequiredActionsSuccessLabel.style.display = 'none';
|
||||||
|
sbRequiredActionsFailureLabel.style.display = 'block';
|
||||||
|
sbRequiredActionsFailureSubtext.style.display = 'block';
|
||||||
|
|
||||||
|
sbActionsButton.title = `You are missing Streamer.bot actions`;
|
||||||
|
|
||||||
|
sbActionsButton.textContent = '⚠️';
|
||||||
|
|
||||||
|
OpenRequiredActionsDialog();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
///////////////////////
|
||||||
|
// PAGE INTERACTIONS //
|
||||||
|
///////////////////////
|
||||||
|
|
||||||
|
function Connect() {
|
||||||
|
sbClientListeners = sbClient.listeners;
|
||||||
|
sbClient.options.host = sbAddressInput.value;
|
||||||
|
sbClient.options.port = sbPortInput.value;
|
||||||
|
sbClient.options.password = sbPasswordInput.value;
|
||||||
|
sbClient.connect();
|
||||||
|
}
|
||||||
|
|
||||||
|
function CopyImportCode() {
|
||||||
|
const textToCopy = sbImportCodeLabel.textContent;
|
||||||
|
navigator.clipboard.writeText(textToCopy)
|
||||||
|
.then(() => {
|
||||||
|
console.debug('Copied to clipboard!');
|
||||||
|
|
||||||
|
// Click feedback
|
||||||
|
const root = document.documentElement;
|
||||||
|
const successColor = getComputedStyle(root).getPropertyValue('--success-color').trim();
|
||||||
|
const buttonColor = getComputedStyle(root).getPropertyValue('--button-color').trim();
|
||||||
|
sbImportCopyButton.textContent = 'Copied!';
|
||||||
|
sbImportCopyButton.style.background = successColor;
|
||||||
|
setTimeout(() => {
|
||||||
|
sbImportCopyButton.textContent = 'Copy';
|
||||||
|
sbImportCopyButton.style.background = buttonColor;
|
||||||
|
}, 1500);
|
||||||
|
})
|
||||||
|
.catch(err => console.error('Failed to copy text: ', err));
|
||||||
|
}
|
||||||
|
|
||||||
|
function OpenConnectDialog() {
|
||||||
|
sbConnectDialog.style.display = "flex";
|
||||||
|
blurLayer.style.display = "block";
|
||||||
|
}
|
||||||
|
|
||||||
|
function ClosenConnectDialog() {
|
||||||
|
sbConnectDialog.style.display = "none";
|
||||||
|
blurLayer.style.display = "none";
|
||||||
|
}
|
||||||
|
|
||||||
|
function OpenRequiredActionsDialog() {
|
||||||
|
sbRequiredActionsDialog.style.display = "flex";
|
||||||
|
blurLayer.style.display = "block";
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,147 @@
|
|||||||
|
html,
|
||||||
|
body {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Needed to force correct emoji representation */
|
||||||
|
span {
|
||||||
|
font-family: "Segoe UI Emoji", "Apple Color Emoji", "Noto Color Emoji", sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**************/
|
||||||
|
/*** HEADER ***/
|
||||||
|
/**************/
|
||||||
|
|
||||||
|
#header {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0em 1em;
|
||||||
|
gap: 1em;
|
||||||
|
box-shadow: 0 0 10px rgba(0, 0, 0, 1);
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
#header-end {
|
||||||
|
margin-left: auto;
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*********************/
|
||||||
|
/*** PAGE CONTENTS ***/
|
||||||
|
/*********************/
|
||||||
|
|
||||||
|
#content {
|
||||||
|
flex: 1;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/********************************/
|
||||||
|
/*** STREAMER.BOT CONNECT BOX ***/
|
||||||
|
/********************************/
|
||||||
|
|
||||||
|
#sb-required-actions-dialog {
|
||||||
|
max-width: 80%;
|
||||||
|
max-height: 80%;
|
||||||
|
overflow: auto;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sb-connect-dialog {
|
||||||
|
max-width: 90%;
|
||||||
|
/* never wider than 90% of viewport */
|
||||||
|
width: 90%;
|
||||||
|
/* scale with window */
|
||||||
|
max-height: 90vh;
|
||||||
|
/* never taller than 90% of viewport */
|
||||||
|
overflow-y: auto;
|
||||||
|
/* vertical scroll if content overflows */
|
||||||
|
overflow-x: hidden;
|
||||||
|
/* prevent horizontal scroll */
|
||||||
|
box-sizing: border-box;
|
||||||
|
/* include padding in width/height */
|
||||||
|
}
|
||||||
|
|
||||||
|
#sb-connect-dialog {
|
||||||
|
width: 90%;
|
||||||
|
max-width: 30em;
|
||||||
|
max-height: 90vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
box-sizing: border-box;
|
||||||
|
position: fixed;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
padding: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sb-connect-dialog>div:nth-of-type(2) {
|
||||||
|
display: flex;
|
||||||
|
gap: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sb-connect-dialog .field {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sb-connect-button:enabled:hover {
|
||||||
|
background-color: #3be477;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sb-error-label {
|
||||||
|
background: var(--error-background);
|
||||||
|
border-radius: 0.5em;
|
||||||
|
padding: 1em;
|
||||||
|
color: var(--error-color);
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/******************************************/
|
||||||
|
/*** STREAMER.BOT REQUIRED ACTIONS LIST ***/
|
||||||
|
/******************************************/
|
||||||
|
|
||||||
|
.sb-required-action {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sb-required-action-found {
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sb-import-code {
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow-x: auto;
|
||||||
|
overflow-y: hidden;
|
||||||
|
width: 100%;
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 1em;
|
||||||
|
padding: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sb-import-wrapper {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5em;
|
||||||
|
max-width: 30em;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sb-import-copy-button {
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 207 KiB |
|
After Width: | Height: | Size: 30 KiB |
@@ -0,0 +1,370 @@
|
|||||||
|
/*****************/
|
||||||
|
/*** VARIABLES ***/
|
||||||
|
/*****************/
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--domain: nutty;
|
||||||
|
--background-color: #181818;
|
||||||
|
--accent-color: #2196f3;
|
||||||
|
--success-color: #3be477;
|
||||||
|
--confirm-flash: #3be47680;
|
||||||
|
--button-color: #2e2e2e;
|
||||||
|
--dialog-background: #1d1d1d;
|
||||||
|
--callout-background: #181818;
|
||||||
|
--error-background: #a82d2e33;
|
||||||
|
--error-color: #c65e5e;
|
||||||
|
--mandatory-field-color: #c93f3f;
|
||||||
|
--error-text-color: #c93f3f;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@100..900&display=swap');
|
||||||
|
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
font-family: Inter, system-ui, sans-serif;
|
||||||
|
/* font-family: "Segoe UI Emoji", "Apple Color Emoji", "Noto Color Emoji", sans-serif; */
|
||||||
|
/* font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica, Arial, sans-serif; */
|
||||||
|
color: white;
|
||||||
|
background-color: var(--background-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/************/
|
||||||
|
/*** TEXT ***/
|
||||||
|
/************/
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-weight: 900;
|
||||||
|
font-size: 1.2em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-description {
|
||||||
|
font-size: 0.9em;
|
||||||
|
font-weight: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-attribute {
|
||||||
|
font-size: 0.8em;
|
||||||
|
font-weight: 200;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/***************/
|
||||||
|
/*** DIVIDER ***/
|
||||||
|
/***************/
|
||||||
|
|
||||||
|
.divider {
|
||||||
|
border: none;
|
||||||
|
/* border-top: 0.1px solid #444444; */
|
||||||
|
margin: 0.3em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/***************/
|
||||||
|
/*** BUTTONS ***/
|
||||||
|
/***************/
|
||||||
|
|
||||||
|
button {
|
||||||
|
font-size: 1em;
|
||||||
|
font-weight: 600;
|
||||||
|
background-color: var(--button-color);
|
||||||
|
color: white;
|
||||||
|
opacity: 0.8;
|
||||||
|
border-width: 0;
|
||||||
|
border-radius: 0.5em;
|
||||||
|
border: 1px solid #404040;
|
||||||
|
padding: 0.5em 1em;
|
||||||
|
width: 100%;
|
||||||
|
transition: all 0.2s ease-in-out;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover {
|
||||||
|
opacity: 1;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:disabled {
|
||||||
|
opacity: 0.3;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-button {
|
||||||
|
width: 1em;
|
||||||
|
height: 1em;
|
||||||
|
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
font-size: 1.1em;
|
||||||
|
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: 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/***********************/
|
||||||
|
/*** SLIDER SWITCHES ***/
|
||||||
|
/***********************/
|
||||||
|
|
||||||
|
/* Switch styling */
|
||||||
|
.switch {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
width: 3em;
|
||||||
|
height: 1.5em;
|
||||||
|
font-size: 1em;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch input {
|
||||||
|
opacity: 0;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider {
|
||||||
|
position: absolute;
|
||||||
|
cursor: pointer;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background-color: #ccc;
|
||||||
|
transition: 0.4s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider:before {
|
||||||
|
position: absolute;
|
||||||
|
content: "";
|
||||||
|
height: 1.1em;
|
||||||
|
width: 1.1em;
|
||||||
|
left: 0.2em;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
background-color: white;
|
||||||
|
transition: 0.4s;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
input:checked+.slider {
|
||||||
|
background-color: var(--accent-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
input:focus+.slider {
|
||||||
|
box-shadow: 0 0 1px var(--accent-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
input:checked+.slider:before {
|
||||||
|
transform: translate(1.5em, -50%);
|
||||||
|
/* move knob across, stay centered */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Rounded sliders */
|
||||||
|
.slider.round {
|
||||||
|
border-radius: 1.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider.round:before {
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/******************/
|
||||||
|
/*** SCROLLBARS ***/
|
||||||
|
/******************/
|
||||||
|
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: #2c2c2c;
|
||||||
|
/* Color of the tracking area */
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background-color: #9f9f9f;
|
||||||
|
/* Color of the scroll thumb */
|
||||||
|
border-radius: 4px;
|
||||||
|
/* Roundness of the scroll thumb */
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
background-color: #d1d1d1;
|
||||||
|
/* Color of the scroll thumb on hover */
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/***************/
|
||||||
|
/*** IFRAMES ***/
|
||||||
|
/***************/
|
||||||
|
|
||||||
|
iframe {
|
||||||
|
border-width: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/***************/
|
||||||
|
/*** CALLOUT ***/
|
||||||
|
/***************/
|
||||||
|
|
||||||
|
.callout {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**************/
|
||||||
|
/*** DIALOG ***/
|
||||||
|
/**************/
|
||||||
|
|
||||||
|
.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;
|
||||||
|
|
||||||
|
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 {
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
font-size: 1.5em;
|
||||||
|
font-weight: 100;
|
||||||
|
padding: 0em;
|
||||||
|
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 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg width="100%" height="100%" viewBox="0 0 2000 1556" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||||
|
<g transform="matrix(30.772,0,0,30.772,323.017,878.74)">
|
||||||
|
<path d="M32.927,8.977L22,22L11.073,8.977C14.134,6.408 18.003,5 22,5C25.997,5 29.866,6.408 32.927,8.977ZM-5.271,-10.5L-10.497,-16.729C-1.392,-24.369 10.114,-28.557 22,-28.557C33.886,-28.557 45.392,-24.369 54.497,-16.729L49.271,-10.5C41.63,-16.912 31.974,-20.426 22,-20.426C12.026,-20.426 2.37,-16.912 -5.271,-10.5ZM4.373,0.993L0.145,-4.046C6.269,-9.184 14.007,-12 22,-12C29.993,-12 37.731,-9.184 43.855,-4.046L39.627,0.993C34.688,-3.151 28.447,-5.422 22,-5.422C15.553,-5.422 9.312,-3.151 4.373,0.993Z" style="fill:rgb(0,230,118);"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.0 KiB |
@@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg width="100%" height="100%" viewBox="0 0 1865 1556" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||||
|
<g transform="matrix(86.4291,0,0,86.4291,-901.44,-432.146)">
|
||||||
|
<path d="M32,9L32,15L30,15L30,9L32,9ZM32,17L32,19L30,19L30,17L32,17ZM28.048,9.158C26.155,8.331 24.097,7.895 22,7.895C18.449,7.895 15.011,9.146 12.291,11.429L10.43,9.211C13.672,6.491 17.768,5 22,5C24.923,5 27.782,5.712 30.339,7.048L30,7.048C28.922,7.048 28.048,7.922 28.048,9L28.048,9.158ZM28.082,15.362C26.355,13.988 24.212,13.237 22,13.237C19.705,13.237 17.483,14.045 15.724,15.521L14.219,13.727C16.399,11.897 19.154,10.895 22,10.895C24.135,10.895 26.22,11.459 28.048,12.514L28.048,15C28.048,15.124 28.06,15.245 28.082,15.362ZM25.891,18.363L22,23L18.109,18.363C19.2,17.449 20.577,16.947 22,16.947C23.423,16.947 24.8,17.449 25.891,18.363Z" style="fill:rgb(221,44,0);"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -0,0 +1,81 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<link rel="shortcut icon" href="#" />
|
||||||
|
<link rel="stylesheet" href="./style.css" />
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@xz/fonts@1/serve/metropolis.min.css" />
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="refreshContainer">Refreshed</div>
|
||||||
|
|
||||||
|
<div id="instructionsContainer">
|
||||||
|
<div id="streamerbotConnectInstructions" style="display: none;">
|
||||||
|
<div style="padding: 10px 20px;">
|
||||||
|
Please connect to Streamer.bot.
|
||||||
|
<br>
|
||||||
|
Click <a class="hyperlinks" href="https://nuttylmao.notion.site/Multistream-Title-Updater-e2fb7b24c7514f9cbd943729f54be1d9" target="_blank">here</a> for instructions.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="missingActionsInstructions" style="display: none; ">
|
||||||
|
<div style="padding: 10px 20px;">
|
||||||
|
Streamer.bot is connected, but you are missing actions.
|
||||||
|
Please import <label style="color: #ffb700;">multistream-title-updater.nut</label> and refresh the page.
|
||||||
|
Click <a class="hyperlinks" href="https://nuttylmao.notion.site/Multistream-Title-Updater-e2fb7b24c7514f9cbd943729f54be1d9" target="_blank">here</a> for instructions.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div id="mainContainer">
|
||||||
|
|
||||||
|
<div class="straightUpIloveCum">
|
||||||
|
<div id="title" style="font-size: 20px;">Update Titles</div>
|
||||||
|
<button id="infoIcon" style="padding: 0px 10px;">
|
||||||
|
<img src="info.svg" height="16px"/>
|
||||||
|
</button>
|
||||||
|
<img src="disconnected.svg" id="connectionStatusIcon"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
<div id="ThisIsWhereAllTheCoolStuffHappens">
|
||||||
|
<div>
|
||||||
|
<input type="text" id="titleInput" class="textBox"><br><br>
|
||||||
|
<div class="straightUpIloveCum">
|
||||||
|
<input type="submit" id="submitButton" class="button submitButton" value="UPDATE ALL">
|
||||||
|
<input type="submit" id="refreshButton" class="button" value="REFRESH">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
<div id="fieldsContainer">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<template id="platformTemplate">
|
||||||
|
<div class="broadcastBox">
|
||||||
|
<button id="platformIconButton" style="padding: 0px 10px;">
|
||||||
|
<img id="icon" height="20px" style="padding: 0px 10px 0px 0px;"/>
|
||||||
|
</button>
|
||||||
|
<input type="text" class="textBox platformTitle">
|
||||||
|
<input type="submit" class="button submitButton platformSubmitButton" value="UPDATE">
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div id="noYouTubeStreamsInstructions" style="display: none; text-align: center; margin: 10px; font-weight: bold; opacity: 0.5;">
|
||||||
|
<div>
|
||||||
|
⚠️ YouTube titles can only be updated after the stream has started ⚠️
|
||||||
|
<br><br>
|
||||||
|
Please start your stream to update YouTube titles
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
<script src='https://cdnjs.cloudflare.com/ajax/libs/gsap/1.18.0/TweenMax.min.js'></script>
|
||||||
|
<script src="./script.js"></script>
|
||||||
|
Before Width: | Height: | Size: 937 B After Width: | Height: | Size: 937 B |
@@ -1,4 +1,4 @@
|
|||||||
[NUT] Multistream Title Updater | Fetch Broadcasts
|
[NUT] Multistream Title Updater | Fetch Broadcasts
|
||||||
[NUT] Multistream Title Updater | Update All Broadcasts
|
[NUT] Multistream Title Updater | Update All Broadcasts
|
||||||
[NUT] Multistream Title Updater | Update Twitch Title
|
[NUT] Multistream Title Updater | Update Twitch Title
|
||||||
[NUT] Multistream Title Updater | Update YouTube Title
|
[NUT] Multistream Title Updater | Update YouTube Title
|
||||||
@@ -0,0 +1,444 @@
|
|||||||
|
////////////
|
||||||
|
// FIELDS //
|
||||||
|
////////////
|
||||||
|
|
||||||
|
let sbDebugMode = true;
|
||||||
|
|
||||||
|
const queryString = window.location.search;
|
||||||
|
const urlParams = new URLSearchParams(queryString);
|
||||||
|
|
||||||
|
const sbServerPort = urlParams.get("port") || 8080;
|
||||||
|
const sbServerAddress = urlParams.get("server") || "127.0.0.1";
|
||||||
|
|
||||||
|
/////////////////
|
||||||
|
// GLOBAL VARS //
|
||||||
|
/////////////////
|
||||||
|
|
||||||
|
let ws;
|
||||||
|
|
||||||
|
///////////////////////////////////
|
||||||
|
// SRTEAMER.BOT WEBSOCKET SERVER //
|
||||||
|
///////////////////////////////////
|
||||||
|
|
||||||
|
// This is the main function that connects to the Streamer.bot websocke server
|
||||||
|
function connectws() {
|
||||||
|
if ("WebSocket" in window) {
|
||||||
|
ws = new WebSocket("ws://" + sbServerAddress + ":" + sbServerPort + "/");
|
||||||
|
|
||||||
|
// Reconnect
|
||||||
|
ws.onclose = function () {
|
||||||
|
SetConnectionStatus(false);
|
||||||
|
setTimeout(connectws, 5000);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Connect
|
||||||
|
ws.onopen = async function () {
|
||||||
|
SetConnectionStatus(true);
|
||||||
|
|
||||||
|
console.log("Subscribe to events");
|
||||||
|
ws.send(
|
||||||
|
JSON.stringify({
|
||||||
|
request: "Subscribe",
|
||||||
|
id: "subscribe-events-id",
|
||||||
|
// This is the list of Streamer.bot websocket events to subscribe to
|
||||||
|
// See full list of events here:
|
||||||
|
// https://docs.streamer.bot/api/servers/websocket/requests
|
||||||
|
events: {
|
||||||
|
twitch: [
|
||||||
|
"StreamUpdate"
|
||||||
|
],
|
||||||
|
youTube: [
|
||||||
|
"BroadcastStarted",
|
||||||
|
"BroadcastEnded",
|
||||||
|
"BroadcastUpdated"
|
||||||
|
],
|
||||||
|
general: [
|
||||||
|
"Custom"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
sbGetActions(ws);
|
||||||
|
|
||||||
|
ws.onmessage = function (event) {
|
||||||
|
// Grab message and parse JSON
|
||||||
|
const msg = event.data;
|
||||||
|
const wsdata = JSON.parse(msg);
|
||||||
|
|
||||||
|
// Check if the user installed all the required Streamer.bot actions
|
||||||
|
if (wsdata.id == "GetActions") {
|
||||||
|
|
||||||
|
// Check if all the required SB action exist
|
||||||
|
ReadLinesFromFile('requiredActions.txt')
|
||||||
|
.then(requiredActions => {
|
||||||
|
if (sbCheckRequiredActions(wsdata.actions, requiredActions)) {
|
||||||
|
SetElementVisibility("missingActionsInstructions", false);
|
||||||
|
sbFetchBroadcasts(ws);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
SetElementVisibility("missingActionsInstructions", true);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof wsdata.event == "undefined") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print data to log for debugging purposes
|
||||||
|
if (sbDebugMode) {
|
||||||
|
console.log(wsdata.data);
|
||||||
|
console.log(wsdata.event.source);
|
||||||
|
console.log(wsdata.event.type);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for events to trigger
|
||||||
|
// See documentation for all events here:
|
||||||
|
// https://wiki.streamer.bot/en/Servers-Clients/WebSocket-Server/Events
|
||||||
|
switch (wsdata.event.source) {
|
||||||
|
// Twitch Events
|
||||||
|
case 'Twitch':
|
||||||
|
switch (wsdata.event.type) {
|
||||||
|
case ('StreamUpdate'):
|
||||||
|
sbFetchBroadcasts(ws);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// Twitch Events
|
||||||
|
case 'YouTube':
|
||||||
|
switch (wsdata.event.type) {
|
||||||
|
case ('BroadcastStarted'):
|
||||||
|
case ('BroadcastEnded'):
|
||||||
|
case ('BroadcastUpdated'):
|
||||||
|
sbFetchBroadcasts(ws);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// General Events
|
||||||
|
case 'General':
|
||||||
|
switch (wsdata.event.type) {
|
||||||
|
case ('Custom'):
|
||||||
|
switch (wsdata.data.action) {
|
||||||
|
case ('[NUT] Multistream Title Updater | Fetch Broadcasts'):
|
||||||
|
UpdateBroadcastList(wsdata.data);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/////////////////////////
|
||||||
|
// STREAMER.BOT WIDGET //
|
||||||
|
/////////////////////////
|
||||||
|
|
||||||
|
function sbGetActions(ws) {
|
||||||
|
|
||||||
|
let request = JSON.stringify({
|
||||||
|
request: "GetActions",
|
||||||
|
id: "GetActions"
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.send(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if all the entries in targetActionNames exist in actionList
|
||||||
|
function sbCheckRequiredActions(actionList, targetActionNames) {
|
||||||
|
let foundActions = 0;
|
||||||
|
for (targetActionName of targetActionNames) {
|
||||||
|
if (actionList.some(action => action.name === targetActionName))
|
||||||
|
foundActions++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return foundActions == targetActionNames.length
|
||||||
|
}
|
||||||
|
|
||||||
|
function sbFetchBroadcasts(ws) {
|
||||||
|
|
||||||
|
let request = JSON.stringify({
|
||||||
|
request: "DoAction",
|
||||||
|
id: generateUUID(),
|
||||||
|
action: {
|
||||||
|
name: "[NUT] Multistream Title Updater | Fetch Broadcasts"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.send(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
function sbUpdateTitles(ws, title) {
|
||||||
|
let request = JSON.stringify({
|
||||||
|
request: "DoAction",
|
||||||
|
id: generateUUID(),
|
||||||
|
action: {
|
||||||
|
name: "[NUT] Multistream Title Updater | Update All Broadcasts"
|
||||||
|
},
|
||||||
|
args: {
|
||||||
|
title: title
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.send(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
function sbUpdateTwitchTitle(ws, title) {
|
||||||
|
let request = JSON.stringify({
|
||||||
|
request: "DoAction",
|
||||||
|
id: generateUUID(),
|
||||||
|
action: {
|
||||||
|
name: "[NUT] Multistream Title Updater | Update Twitch Title"
|
||||||
|
},
|
||||||
|
args: {
|
||||||
|
title: title
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.send(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
function sbUpdateYouTubeTitle(ws, title, broadcastId) {
|
||||||
|
let request = JSON.stringify({
|
||||||
|
request: "DoAction",
|
||||||
|
id: generateUUID(),
|
||||||
|
action: {
|
||||||
|
name: "[NUT] Multistream Title Updater | Update YouTube Title"
|
||||||
|
},
|
||||||
|
args: {
|
||||||
|
broadcastId: broadcastId,
|
||||||
|
title: title
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.send(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//////////////////////
|
||||||
|
// HELPER FUNCTIONS //
|
||||||
|
//////////////////////
|
||||||
|
|
||||||
|
function generateUUID() { // Public Domain/MIT
|
||||||
|
var d = new Date().getTime();//Timestamp
|
||||||
|
var d2 = ((typeof performance !== 'undefined') && performance.now && (performance.now() * 1000)) || 0;//Time in microseconds since page-load or 0 if unsupported
|
||||||
|
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
|
||||||
|
var r = Math.random() * 16;//random number between 0 and 16
|
||||||
|
if (d > 0) {//Use timestamp until depleted
|
||||||
|
r = (d + r) % 16 | 0;
|
||||||
|
d = Math.floor(d / 16);
|
||||||
|
} else {//Use microseconds since page-load if supported
|
||||||
|
r = (d2 + r) % 16 | 0;
|
||||||
|
d2 = Math.floor(d2 / 16);
|
||||||
|
}
|
||||||
|
return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function IsNullOrWhitespace(str) {
|
||||||
|
return /^\s*$/.test(str);
|
||||||
|
}
|
||||||
|
|
||||||
|
function SetElementVisibility(elementID, visibility) {
|
||||||
|
let element = document.getElementById(elementID);
|
||||||
|
if (visibility)
|
||||||
|
element.style.display = 'inline';
|
||||||
|
else
|
||||||
|
element.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
function ReadLinesFromFile(filePath) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const request = new XMLHttpRequest();
|
||||||
|
request.open('GET', filePath, true);
|
||||||
|
request.onload = function () {
|
||||||
|
if (request.status === 200) {
|
||||||
|
resolve(request.responseText.split(/\r?\n/));
|
||||||
|
} else {
|
||||||
|
reject(new Error(`File loading failed with status: ${request.status}`));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
request.onerror = function () {
|
||||||
|
reject(new Error('Network error occurred during file loading.'));
|
||||||
|
};
|
||||||
|
request.send();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
///////////////////////////////////
|
||||||
|
// STREAMER.BOT WEBSOCKET STATUS //
|
||||||
|
///////////////////////////////////
|
||||||
|
|
||||||
|
// This function sets the visibility of the Streamer.bot status label on the overlay
|
||||||
|
function SetConnectionStatus(connected) {
|
||||||
|
let connectionStatusIcon = document.getElementById("connectionStatusIcon");
|
||||||
|
let ThisIsWhereAllTheCoolStuffHappens = document.getElementById("ThisIsWhereAllTheCoolStuffHappens");
|
||||||
|
|
||||||
|
if (connected) {
|
||||||
|
connectionStatusIcon.src = "connected.svg";
|
||||||
|
ThisIsWhereAllTheCoolStuffHappens.classList.remove('disabled');
|
||||||
|
SetElementVisibility("infoIcon", false);
|
||||||
|
SetElementVisibility("streamerbotConnectInstructions", false);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
connectionStatusIcon.src = "disconnected.svg";
|
||||||
|
ThisIsWhereAllTheCoolStuffHappens.classList.add('disabled');
|
||||||
|
SetElementVisibility("infoIcon", true);
|
||||||
|
SetElementVisibility("streamerbotConnectInstructions", true);
|
||||||
|
|
||||||
|
// (1) Clear list of broadcasts
|
||||||
|
const fieldsContainer = document.getElementById('fieldsContainer');
|
||||||
|
fieldsContainer.innerHTML = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// This function sets the visibility of the Streamer.bot status label on the overlay
|
||||||
|
function RefreshAnimation() {
|
||||||
|
let refreshContainer = document.getElementById("refreshContainer");
|
||||||
|
refreshContainer.style.opacity = 1;
|
||||||
|
var tl = new TimelineMax();
|
||||||
|
tl
|
||||||
|
.to(refreshContainer, 2, { opacity: 0, ease: Linear.easeNone })
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// Button handling for UPDATE ALL button only
|
||||||
|
const infoIcon = document.querySelector("#infoIcon");
|
||||||
|
infoIcon.addEventListener("click", function () {
|
||||||
|
window.open("https://www.notion.so/nutty-s-multistream-title-updater-e2fb7b24c7514f9cbd943729f54be1d9");
|
||||||
|
});
|
||||||
|
|
||||||
|
// Button handling for UPDATE ALL button only
|
||||||
|
const submitButton = document.querySelector("#submitButton");
|
||||||
|
submitButton.addEventListener("click", function () {
|
||||||
|
const titleInput = document.querySelector("#titleInput").value;
|
||||||
|
sbUpdateTitles(ws, titleInput);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Button handling for REFRESH button only
|
||||||
|
const refreshButton = document.querySelector("#refreshButton");
|
||||||
|
refreshButton.addEventListener("click", function () {
|
||||||
|
sbFetchBroadcasts(ws);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
function UpdateBroadcastList(data) {
|
||||||
|
// Iterate through broadcast list
|
||||||
|
// Find div whose ID matches the broadcast
|
||||||
|
// If it exists, update the title
|
||||||
|
// else, it's a new broadcast, so add it to the list
|
||||||
|
const fieldsContainer = document.getElementById('fieldsContainer');
|
||||||
|
for (const broadcast of data.broadcastList) {
|
||||||
|
const broadcastDiv = fieldsContainer.querySelector(`#${broadcast.id}`);
|
||||||
|
|
||||||
|
if (broadcastDiv == null)
|
||||||
|
AddBroadcast(broadcast);
|
||||||
|
else
|
||||||
|
UpdateBroadcast(broadcastDiv, broadcast);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if any broadcasts have gone offline
|
||||||
|
// If so, delete them from the list
|
||||||
|
var parentDiv = document.getElementById('fieldsContainer');
|
||||||
|
const childDivs = parentDiv.querySelectorAll("div");
|
||||||
|
childDivs.forEach(childDiv => {
|
||||||
|
var result = data.broadcastList.find(obj => {
|
||||||
|
return obj.id === childDiv.id
|
||||||
|
})
|
||||||
|
if (result == null)
|
||||||
|
childDiv.innerHTML = "";
|
||||||
|
});
|
||||||
|
|
||||||
|
//// THIS CODE ALSO WORKS AND IS WAY SIMPLER, I JUST MADE THINGS
|
||||||
|
//// 10 TIMES HARDER FOR MYSELF BECAUSE I LOVE PAIN POGGERS
|
||||||
|
// // (1) Clear list of broadcasts
|
||||||
|
// const fieldsContainer = document.getElementById('fieldsContainer');
|
||||||
|
// fieldsContainer.innerHTML = "";
|
||||||
|
|
||||||
|
// // (2) For each broadcast in list, create new entry
|
||||||
|
// for (const broadcast of data.broadcastList)
|
||||||
|
// {
|
||||||
|
// AddBroadcast(broadcast);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// Count the number of YouTube broadcasts
|
||||||
|
// If this is 0, put a message to tell the user that they need to go live on YouTube first
|
||||||
|
// Else, hide that message
|
||||||
|
const youtubeBroadcastCount = data.broadcastList.reduce((count, broadcast) => count + (broadcast.platform === "youtube" ? 1 : 0), 0);
|
||||||
|
if (youtubeBroadcastCount <= 0)
|
||||||
|
SetElementVisibility("noYouTubeStreamsInstructions", true);
|
||||||
|
else
|
||||||
|
SetElementVisibility("noYouTubeStreamsInstructions", false);
|
||||||
|
|
||||||
|
RefreshAnimation();
|
||||||
|
}
|
||||||
|
|
||||||
|
function AddBroadcast(broadcast) {
|
||||||
|
// Get a reference to the template
|
||||||
|
const template = document.getElementById('platformTemplate');
|
||||||
|
|
||||||
|
// Create a new instance of the template
|
||||||
|
const instance = template.content.cloneNode(true);
|
||||||
|
|
||||||
|
// Assign ID to instance
|
||||||
|
const broadcastBox = instance.querySelector('.broadcastBox');
|
||||||
|
broadcastBox.id = broadcast.id;
|
||||||
|
|
||||||
|
// Modify the content of the template instance
|
||||||
|
const titleElement = instance.querySelector('.platformTitle');
|
||||||
|
titleElement.value = broadcast.title;
|
||||||
|
|
||||||
|
// Modify the content of the template instance
|
||||||
|
const buttonElement = instance.querySelector('.platformSubmitButton');
|
||||||
|
buttonElement.classList.add(broadcast.platform);
|
||||||
|
|
||||||
|
// Modify the icon of the template instance
|
||||||
|
const iconElement = instance.querySelector('#icon');
|
||||||
|
|
||||||
|
// Add button handling (need separate handling for Twitch/YouTube)
|
||||||
|
switch (broadcast.platform) {
|
||||||
|
case 'twitch':
|
||||||
|
iconElement.src = 'twitch.png';
|
||||||
|
|
||||||
|
buttonElement.addEventListener("click", function () {
|
||||||
|
sbUpdateTwitchTitle(ws, titleElement.value);
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case 'youtube':
|
||||||
|
// Modify the icon of the template instance
|
||||||
|
iconElement.src = 'youtube.png';
|
||||||
|
|
||||||
|
buttonElement.addEventListener("click", function () {
|
||||||
|
const youtubeID = broadcast.id.replace('youtube-', '');
|
||||||
|
sbUpdateYouTubeTitle(ws, titleElement.value, youtubeID);
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add click event
|
||||||
|
const iconButton = instance.querySelector('#platformIconButton');
|
||||||
|
iconButton.addEventListener("click", function () {
|
||||||
|
window.open(broadcast.url);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Insert the modified template instance into the DOM
|
||||||
|
const fieldsContainer = document.getElementById('fieldsContainer');
|
||||||
|
fieldsContainer.appendChild(instance);
|
||||||
|
}
|
||||||
|
|
||||||
|
function UpdateBroadcast(div, broadcast) {
|
||||||
|
// Modify the content of the template instance
|
||||||
|
const titleElement = div.querySelector('.platformTitle');
|
||||||
|
titleElement.value = broadcast.title;
|
||||||
|
}
|
||||||
|
|
||||||
|
connectws();
|
||||||
|
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 55 KiB |
|
Before Width: | Height: | Size: 65 KiB After Width: | Height: | Size: 65 KiB |
@@ -0,0 +1,14 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<link rel="stylesheet" href="../../../.common/styles/global.css" />
|
||||||
|
<link rel="stylesheet" href="./style.css" />
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
|
|
||||||
|
<script src="script.js"></script>
|
||||||
@@ -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 //
|
||||||
|
///////////////////
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
body {
|
||||||
|
margin: 1em;
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
|
||||||
|
<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>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<iframe id="dock-wrapper"></iframe>
|
||||||
|
<script src="script.js"></script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
// Construct URL
|
||||||
|
const currentURL = window.location.href;
|
||||||
|
let baseURL = currentURL;
|
||||||
|
|
||||||
|
if (baseURL.endsWith("index.html"))
|
||||||
|
baseURL = baseURL.replace("index.html", "");
|
||||||
|
|
||||||
|
const configJson = "?config=" + baseURL + "config.json";
|
||||||
|
|
||||||
|
// Implement widget dock core
|
||||||
|
window.dockWrapper = document.getElementById('dock-wrapper');
|
||||||
|
dockWrapper.src = `../../.common/core/widget-dock-core${configJson}`;
|
||||||
|
|
||||||
|
dockWrapper.addEventListener('load', () => {
|
||||||
|
dockWrapper.contentWindow.content.src = baseURL + '/contents';
|
||||||
|
});
|
||||||
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 75 KiB |
|
Before Width: | Height: | Size: 8.4 KiB After Width: | Height: | Size: 8.4 KiB |
@@ -0,0 +1,228 @@
|
|||||||
|
<!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>
|
||||||
@@ -0,0 +1,546 @@
|
|||||||
|
//////////////////////
|
||||||
|
// 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);
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
@@ -1,81 +1,20 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<link rel="shortcut icon" href="#" />
|
<link rel="stylesheet" href="../.common/styles/global.css" />
|
||||||
<link rel="stylesheet" href="./style.css" />
|
<link rel="icon" href="../../.resources/logo.png" type="image/png">
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@xz/fonts@1/serve/metropolis.min.css" />
|
<title>nutty</title>
|
||||||
|
<style>
|
||||||
|
#dock-wrapper {
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div id="refreshContainer">Refreshed</div>
|
<iframe id="dock-wrapper"></iframe>
|
||||||
|
<script src="script.js"></script>
|
||||||
<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>
|
</body>
|
||||||
|
|
||||||
<script src='https://cdnjs.cloudflare.com/ajax/libs/gsap/1.18.0/TweenMax.min.js'></script>
|
</html>
|
||||||
<script src="./script.js"></script>
|
|
||||||
@@ -1,444 +1,16 @@
|
|||||||
////////////
|
// Construct URL
|
||||||
// FIELDS //
|
const currentURL = window.location.href;
|
||||||
////////////
|
let baseURL = currentURL;
|
||||||
|
|
||||||
let sbDebugMode = true;
|
if (baseURL.endsWith("index.html"))
|
||||||
|
baseURL = baseURL.replace("index.html", "");
|
||||||
|
|
||||||
const queryString = window.location.search;
|
const configJson = "?config=" + baseURL + "config.json";
|
||||||
const urlParams = new URLSearchParams(queryString);
|
|
||||||
|
|
||||||
const sbServerPort = urlParams.get("port") || 8080;
|
// Implement widget dock core
|
||||||
const sbServerAddress = urlParams.get("server") || "127.0.0.1";
|
window.dockWrapper = document.getElementById('dock-wrapper');
|
||||||
|
dockWrapper.src = `../../.common/core/widget-dock-core${configJson}`;
|
||||||
|
|
||||||
/////////////////
|
dockWrapper.addEventListener('load', () => {
|
||||||
// GLOBAL VARS //
|
dockWrapper.contentWindow.content.src = baseURL + '/contents';
|
||||||
/////////////////
|
});
|
||||||
|
|
||||||
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();
|
|
||||||
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 8.4 KiB |
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 30 KiB |
@@ -0,0 +1,76 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<link rel="stylesheet" href="../../../.common/styles/global.css" />
|
||||||
|
<link rel="stylesheet" href="./style.css" />
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/luxon@3/build/global/luxon.min.js"></script>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="settings">
|
||||||
|
<div class="setting">
|
||||||
|
<div>
|
||||||
|
<div class="setting-label" for="printer-name">Printer Name</div>
|
||||||
|
<div class="setting-description">As labelled in Windows > Settings > Printers & Scanners</div>
|
||||||
|
</div>
|
||||||
|
<input type="text" id="printer-name" name="printer-name" placeholder="" value="" autocomplete="off">
|
||||||
|
</div>
|
||||||
|
<div class="setting">
|
||||||
|
<div>
|
||||||
|
<div class="setting-label" for="paper-width">Paper Width (mm)</div>
|
||||||
|
<div class="setting-description">Printer Bot was designed for 80mm thermal paper</div>
|
||||||
|
</div>
|
||||||
|
<input type="number" id="paper-width" name="paper-width" placeholder="80" value="80" min="0" autocomplete="off">
|
||||||
|
</div>
|
||||||
|
<div class="setting">
|
||||||
|
<div>
|
||||||
|
<div class="setting-label" for="ignore-test-triggers">Ignore Test Triggers</div>
|
||||||
|
<div class="setting-description">In enabled, 'Test' and 'Simulate' events will be ignored</div>
|
||||||
|
</div>
|
||||||
|
<label class="switch" style="margin-left: auto;">
|
||||||
|
<input type="checkbox" id="ignore-test-triggers" name="ignore-test-triggers">
|
||||||
|
<span class="slider round"></span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="setting">
|
||||||
|
<div>
|
||||||
|
<div class="setting-label" for="delete-temp-files">Delete Temp Files</div>
|
||||||
|
<div class="setting-description">Turn this off for debugging</div>
|
||||||
|
</div>
|
||||||
|
<label class="switch" style="margin-left: auto;">
|
||||||
|
<input type="checkbox" id="delete-temp-files" name="delete-temp-files" checked>
|
||||||
|
<span class="slider round"></span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<button id="test-print-button" onclick="TestPrint()">Test Print</button>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
|
|
||||||
|
<!-- Receipt Template -->
|
||||||
|
<template id="receipt-template">
|
||||||
|
<div id="receipt-container">
|
||||||
|
<!-- Header -->
|
||||||
|
<div id="receipt-header">
|
||||||
|
<img id="receipt-avatar">
|
||||||
|
<div>
|
||||||
|
<div id="receipt-title"></div>
|
||||||
|
<div id="receipt-subtitle"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Contents -->
|
||||||
|
<div id="receipt-content">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
|
<div id="receipt-footer">
|
||||||
|
<img id="receipt-icon" src="">
|
||||||
|
<div id="receipt-date"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script src="script.js"></script>
|
||||||
@@ -0,0 +1,790 @@
|
|||||||
|
//////////////////////
|
||||||
|
// GLOBAL VARIABLES //
|
||||||
|
//////////////////////
|
||||||
|
|
||||||
|
const sbActionPrintRoutine = '5c756513-a1d0-4285-9dbc-21ad34491310';
|
||||||
|
const avatarMap = new Map();
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/////////////////////////
|
||||||
|
// STREAMER.BOT EVENTS //
|
||||||
|
/////////////////////////
|
||||||
|
|
||||||
|
window.parent.sbClient.on('General.Custom', (response) => {
|
||||||
|
console.debug(response.data);
|
||||||
|
CustomEvent(response.data);
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/////////////////
|
||||||
|
// PRINTER BOT //
|
||||||
|
/////////////////
|
||||||
|
|
||||||
|
async function CustomEvent(data) {
|
||||||
|
if (data.actionName != 'Printer Bot | Events')
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Get a reference to the template
|
||||||
|
const template = document.getElementById('receipt-template');
|
||||||
|
|
||||||
|
// Create a new instance of the template
|
||||||
|
const instance = template.content.cloneNode(true);
|
||||||
|
|
||||||
|
// Get divs
|
||||||
|
const headerEl = instance.querySelector('#receipt-header');
|
||||||
|
const contentEl = instance.querySelector('#receipt-content');
|
||||||
|
const footerEl = instance.querySelector('#receipt-footer');
|
||||||
|
const avatarEl = instance.querySelector('#receipt-avatar');
|
||||||
|
const titleEl = instance.querySelector('#receipt-title');
|
||||||
|
const subtitleEl = instance.querySelector('#receipt-subtitle');
|
||||||
|
const iconEl = instance.querySelector('#receipt-icon');
|
||||||
|
const dateEl = instance.querySelector('#receipt-date');
|
||||||
|
|
||||||
|
// Set the main contents
|
||||||
|
switch (data.__source) {
|
||||||
|
// Twitch events
|
||||||
|
case ('TwitchCheer'):
|
||||||
|
{
|
||||||
|
avatarEl.src = await GetAvatar(data.userName, 'twitch');
|
||||||
|
titleEl.innerText = `${data.bits} BITS`;
|
||||||
|
subtitleEl.innerText = `${data.user}`;
|
||||||
|
|
||||||
|
const messageEl = document.createElement('div');
|
||||||
|
messageEl.innerHTML = data.message;
|
||||||
|
|
||||||
|
// Render emotes
|
||||||
|
for (i in data.emotes) {
|
||||||
|
const emoteElement = `<img src="${data.emotes[i].imageUrl}" class="emote"/>`;
|
||||||
|
const emoteName = EscapeRegExp(data.emotes[i].name);
|
||||||
|
|
||||||
|
let regexPattern = emoteName;
|
||||||
|
|
||||||
|
// Check if the emote name consists only of word characters (alphanumeric and underscore)
|
||||||
|
if (/^\w+$/.test(emoteName)) {
|
||||||
|
regexPattern = `\\b${emoteName}\\b`;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// For non-word emotes, ensure they are surrounded by non-word characters or boundaries
|
||||||
|
regexPattern = `(?<=^|[^\\w])${emoteName}(?=$|[^\\w])`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const regex = new RegExp(regexPattern, 'g');
|
||||||
|
messageEl.innerHTML = messageEl.innerHTML.replace(regex, emoteElement);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render cheermotes
|
||||||
|
for (i in data.cheerEmotes) {
|
||||||
|
const bits = data.cheerEmotes[i].bits;
|
||||||
|
const imageUrl = data.cheerEmotes[i].imageUrl;
|
||||||
|
const name = data.cheerEmotes[i].name;
|
||||||
|
const cheerEmoteElement = `<img src="${imageUrl}" class="emote"/>`;
|
||||||
|
const bitsElements = `<span class="bits">${bits}</span>`
|
||||||
|
messageEl.innerHTML = messageEl.innerHTML.replace(new RegExp(`\\b${name}${bits}\\b`, 'i'), cheerEmoteElement + bitsElements);
|
||||||
|
}
|
||||||
|
|
||||||
|
contentEl.appendChild(messageEl);
|
||||||
|
|
||||||
|
// Set the platform icon
|
||||||
|
SetPlatformIcon(iconEl, 'twitch');
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case ('TwitchSub'):
|
||||||
|
{
|
||||||
|
avatarEl.src = await GetAvatar(data.userName, 'twitch');
|
||||||
|
titleEl.innerText = `${data.tier} subscriber`;
|
||||||
|
subtitleEl.innerText = `${data.user}`;
|
||||||
|
|
||||||
|
const messageEl = document.createElement('div');
|
||||||
|
messageEl.innerHTML = '<b>First time subscriber!</b>';
|
||||||
|
|
||||||
|
contentEl.appendChild(messageEl);
|
||||||
|
|
||||||
|
// Set the platform icon
|
||||||
|
SetPlatformIcon(iconEl, 'twitch');
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case ('TwitchReSub'):
|
||||||
|
{
|
||||||
|
avatarEl.src = await GetAvatar(data.userName, 'twitch');
|
||||||
|
titleEl.innerText = `${data.tier} subscriber`;
|
||||||
|
subtitleEl.innerText = `${data.user}`;
|
||||||
|
|
||||||
|
const messageEl = document.createElement('div');
|
||||||
|
messageEl.innerHTML = `<b>${data.cumulative} months${data.monthStreak > 1 ? ' (' + data.monthStreak + ' in a row!)' : ''}</b>`;
|
||||||
|
if (data.messageStripped)
|
||||||
|
messageEl.innerHTML += `<br><br><i>${data.messageStripped}</i>`;
|
||||||
|
|
||||||
|
contentEl.appendChild(messageEl);
|
||||||
|
|
||||||
|
// Set the platform icon
|
||||||
|
SetPlatformIcon(iconEl, 'twitch');
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case ('TwitchGiftSub'):
|
||||||
|
{
|
||||||
|
// Don't put profile pictures for subs that come from Gift Bombs to avoid rate limits
|
||||||
|
if (data.fromGiftBomb)
|
||||||
|
//avatarEl.style.display = 'none';
|
||||||
|
return;
|
||||||
|
else
|
||||||
|
avatarEl.src = await GetAvatar(data.recipientUserName, 'twitch');
|
||||||
|
titleEl.innerText = `Gifted Sub`;
|
||||||
|
|
||||||
|
const messageEl = document.createElement('div');
|
||||||
|
if (data.anonymous)
|
||||||
|
messageEl.innerHTML += `<b>✦・゚ A mysterious admirer ・゚✦</b><br>`;
|
||||||
|
else
|
||||||
|
messageEl.innerHTML += `<b>${data.user}</b><br>`;
|
||||||
|
messageEl.innerHTML += `gifted a ${data.tier} sub to<br><b>${data.recipientUser}</b>`;
|
||||||
|
|
||||||
|
contentEl.appendChild(messageEl);
|
||||||
|
|
||||||
|
// Set the platform icon
|
||||||
|
SetPlatformIcon(iconEl, 'twitch');
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case ('TwitchGiftBomb'):
|
||||||
|
{
|
||||||
|
avatarEl.src = await GetAvatar(data.userName, 'twitch');
|
||||||
|
titleEl.innerHTML = `${data.gifts} × Gifted Subs`;
|
||||||
|
subtitleEl.innerHTML += `✧*・゚✧ ${data.tier.toUpperCase()} ✧・゚*✧`;
|
||||||
|
if (data.anonymous)
|
||||||
|
subtitleEl.innerHTML += `<br>From a mystery person...`;
|
||||||
|
else
|
||||||
|
subtitleEl.innerHTML += `<br>${data.user}`;
|
||||||
|
|
||||||
|
const messageEl = document.createElement('div');
|
||||||
|
if (data.totalGifts > 1) {
|
||||||
|
messageEl.innerHTML = `They've gifted <b>${data.totalGifts} subs</b> in total!</br></br>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get a list of all recipient users
|
||||||
|
Object.keys(data)
|
||||||
|
.filter(key => /^gift\.recipientUser\d+$/.test(key))
|
||||||
|
.forEach((key, index) => {
|
||||||
|
const username = data[key];
|
||||||
|
messageEl.innerHTML += `${username}</br>`;
|
||||||
|
});
|
||||||
|
|
||||||
|
contentEl.appendChild(messageEl);
|
||||||
|
|
||||||
|
// Set the platform icon
|
||||||
|
SetPlatformIcon(iconEl, 'twitch');
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case ('TwitchRaid'):
|
||||||
|
{
|
||||||
|
avatarEl.src = await GetAvatar(data.userName, 'twitch');
|
||||||
|
|
||||||
|
const messageEl = document.createElement('div');
|
||||||
|
messageEl.innerHTML = `<b>${data.user}</b><br>is raiding with a party of<br><b>${data.viewers} viewers!</b>`;
|
||||||
|
|
||||||
|
contentEl.appendChild(messageEl);
|
||||||
|
|
||||||
|
// Set the platform icon
|
||||||
|
SetPlatformIcon(iconEl, 'twitch');
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
// YouTube Events
|
||||||
|
case ('YouTubeNewSponsor'):
|
||||||
|
{
|
||||||
|
if (data.userProfileUrl)
|
||||||
|
avatarEl.src = data.userProfileUrl;
|
||||||
|
else
|
||||||
|
avatarEl.style.display = 'none';
|
||||||
|
|
||||||
|
titleEl.innerText = `${data.levelName}`;
|
||||||
|
subtitleEl.innerText = `${data.user}`;
|
||||||
|
|
||||||
|
contentEl.style.display = 'none';
|
||||||
|
|
||||||
|
// Set the platform icon
|
||||||
|
SetPlatformIcon(iconEl, 'youtube');
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case ('YouTubeGiftMembershipReceived'):
|
||||||
|
{
|
||||||
|
if (data.gifterProfileUrl)
|
||||||
|
avatarEl.src = data.gifterProfileUrl;
|
||||||
|
else
|
||||||
|
avatarEl.style.display = 'none';
|
||||||
|
titleEl.innerText = `Gifted Membership`;
|
||||||
|
|
||||||
|
const messageEl = document.createElement('div');
|
||||||
|
messageEl.innerHTML = `<b>${data.gifterUser}</b><br>gifted a membership to<br><b>${data.user}</b>!`;
|
||||||
|
|
||||||
|
contentEl.appendChild(messageEl);
|
||||||
|
|
||||||
|
// Set the platform icon
|
||||||
|
SetPlatformIcon(iconEl, 'youtube');
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case ('YouTubeSuperChat'):
|
||||||
|
{
|
||||||
|
if (data.userProfileUrl)
|
||||||
|
avatarEl.src = data.userProfileUrl;
|
||||||
|
else
|
||||||
|
avatarEl.style.display = 'none';
|
||||||
|
titleEl.style.fontSize = '2em';
|
||||||
|
titleEl.innerText = `${data.amount}`;
|
||||||
|
|
||||||
|
const messageEl = document.createElement('div');
|
||||||
|
messageEl.innerHTML = `<b>${data.user}</b><br>sent a Super Chat!`;
|
||||||
|
if (data.message)
|
||||||
|
messageEl.innerHTML += `<br><br><i>${data.message}</i>`;
|
||||||
|
|
||||||
|
contentEl.appendChild(messageEl);
|
||||||
|
|
||||||
|
// Set the platform icon
|
||||||
|
SetPlatformIcon(iconEl, 'youtube');
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case ('YouTubeSuperSticker'):
|
||||||
|
{
|
||||||
|
if (data.stickerImageUrl)
|
||||||
|
avatarEl.src = data.stickerImageUrl;
|
||||||
|
else
|
||||||
|
avatarEl.style.display = 'none';
|
||||||
|
titleEl.style.fontSize = '2em';
|
||||||
|
titleEl.innerText = `${data.amount}`;
|
||||||
|
|
||||||
|
const messageEl = document.createElement('div');
|
||||||
|
messageEl.innerHTML = `<b>${data.user}</b><br>sent a Super Sticker!`;
|
||||||
|
|
||||||
|
contentEl.appendChild(messageEl);
|
||||||
|
|
||||||
|
// Set the platform icon
|
||||||
|
SetPlatformIcon(iconEl, 'youtube');
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
break;
|
||||||
|
|
||||||
|
// Kick Events
|
||||||
|
case ('KickSubscription'):
|
||||||
|
case ('KickResubscription'):
|
||||||
|
{
|
||||||
|
avatarEl.src = ConvertWEBPToPNG(await GetAvatar(data.user, 'kick'));
|
||||||
|
titleEl.innerText = `Subscriber`;
|
||||||
|
subtitleEl.innerText = `${data.user}`;
|
||||||
|
|
||||||
|
const messageEl = document.createElement('div');
|
||||||
|
if (data.duration > 1)
|
||||||
|
messageEl.innerHTML = `<b>${data.duration} months</b>`;
|
||||||
|
else
|
||||||
|
messageEl.innerHTML = '<b>First time subscriber!</b>';
|
||||||
|
|
||||||
|
contentEl.appendChild(messageEl);
|
||||||
|
|
||||||
|
// Set the platform icon
|
||||||
|
SetPlatformIcon(iconEl, 'kick');
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case ('KickGiftSubscription'):
|
||||||
|
{
|
||||||
|
avatarEl.src = ConvertWEBPToPNG(await GetAvatar(data["recipient.userLogin"], 'kick'));
|
||||||
|
titleEl.innerText = `Gifted Sub`;
|
||||||
|
|
||||||
|
const messageEl = document.createElement('div');
|
||||||
|
messageEl.innerHTML = `<b>${data.user}</b><br>gifted a sub to<br><b>${data["recipient.userName"]}</b>!`;
|
||||||
|
|
||||||
|
contentEl.appendChild(messageEl);
|
||||||
|
|
||||||
|
// Set the platform icon
|
||||||
|
SetPlatformIcon(iconEl, 'kick');
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case ('KickMassGiftSubscription'):
|
||||||
|
{
|
||||||
|
// There is only one sub, so use the same template for a single gifted sub
|
||||||
|
if ('recipient.userName' in data) {
|
||||||
|
avatarEl.src = ConvertWEBPToPNG(await GetAvatar(data["recipient.userLogin"], 'kick'));
|
||||||
|
titleEl.innerText = `Gifted Sub`;
|
||||||
|
|
||||||
|
const messageEl = document.createElement('div');
|
||||||
|
messageEl.innerHTML = `<b>${data.user}</b><br>gifted a sub to<br><b>${data["recipient.userName"]}</b>!`;
|
||||||
|
|
||||||
|
contentEl.appendChild(messageEl);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
avatarEl.src = ConvertWEBPToPNG(await GetAvatar(data.user, 'kick'));
|
||||||
|
|
||||||
|
// Calculate how many subs were gived
|
||||||
|
let maxIndex = -1;
|
||||||
|
for (const key in data) {
|
||||||
|
const match = key.match(/^recipient\.(\d+)\./);
|
||||||
|
if (match) {
|
||||||
|
const index = parseInt(match[1], 10);
|
||||||
|
if (index > maxIndex) {
|
||||||
|
maxIndex = index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const totalGifts = maxIndex + 1;
|
||||||
|
|
||||||
|
titleEl.innerHTML = `${totalGifts} × Gifted Subs`;
|
||||||
|
subtitleEl.innerText = `${data.user}`;
|
||||||
|
|
||||||
|
const messageEl = document.createElement('div');
|
||||||
|
|
||||||
|
// Loop through each recipient and include it in the receipt
|
||||||
|
const recipients = {};
|
||||||
|
|
||||||
|
// Reconstruct recipient objects
|
||||||
|
for (const key in data) {
|
||||||
|
const match = key.match(/^recipient\.(\d+)\.(.+)$/);
|
||||||
|
if (match) {
|
||||||
|
const index = match[1];
|
||||||
|
const field = match[2];
|
||||||
|
|
||||||
|
if (!recipients[index]) {
|
||||||
|
recipients[index] = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
recipients[index][field] = data[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Loop through and print userName
|
||||||
|
for (const index in recipients) {
|
||||||
|
messageEl.innerHTML += `${recipients[index].userName}<br>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
contentEl.appendChild(messageEl);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the platform icon
|
||||||
|
SetPlatformIcon(iconEl, 'kick');
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
// StreamElements Events
|
||||||
|
case ('StreamElementsTip'):
|
||||||
|
{
|
||||||
|
const avatarURL = await GetAvatar(data.tipUsername, 'twitch');
|
||||||
|
if (IsValidUrl(avatarURL))
|
||||||
|
avatarEl.src = avatarURL;
|
||||||
|
else
|
||||||
|
avatarEl.style.display = 'none'
|
||||||
|
titleEl.style.fontSize = '2em';
|
||||||
|
titleEl.innerText = FormatCurrency(data.tipAmount, data.tipCurrency);
|
||||||
|
subtitleEl.innerText = `${data.tipUsername}`;
|
||||||
|
|
||||||
|
if (data.tipMessage) {
|
||||||
|
const messageEl = document.createElement('div');
|
||||||
|
messageEl.innerHTML = `<i>${data.tipMessage}</i>`;
|
||||||
|
|
||||||
|
contentEl.appendChild(messageEl);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
contentEl.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
// Streamlabs Events
|
||||||
|
case ('StreamlabsDonation'):
|
||||||
|
{
|
||||||
|
const avatarURL = await GetAvatar(data.donationFrom, 'twitch');
|
||||||
|
if (IsValidUrl(avatarURL))
|
||||||
|
avatarEl.src = avatarURL;
|
||||||
|
else
|
||||||
|
avatarEl.style.display = 'none'
|
||||||
|
titleEl.style.fontSize = '2em';
|
||||||
|
titleEl.innerText = data.donationFormattedAmount;
|
||||||
|
subtitleEl.innerText = `${data.donationFrom}`;
|
||||||
|
|
||||||
|
if (data.donationMessage) {
|
||||||
|
const messageEl = document.createElement('div');
|
||||||
|
messageEl.innerHTML = `<i>${data.donationMessage}</i>`;
|
||||||
|
|
||||||
|
contentEl.appendChild(messageEl);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
contentEl.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
// Fourthwall Events
|
||||||
|
case ('FourthwallDonation'):
|
||||||
|
{
|
||||||
|
const avatarURL = await GetAvatar(data["fw.username"], 'twitch');
|
||||||
|
if (IsValidUrl(avatarURL))
|
||||||
|
avatarEl.src = avatarURL;
|
||||||
|
else
|
||||||
|
avatarEl.style.display = 'none'
|
||||||
|
titleEl.style.fontSize = '2em';
|
||||||
|
titleEl.innerText = FormatCurrency(data["fw.amount"], data["fw.currency"]);
|
||||||
|
if (data["fw.username"])
|
||||||
|
subtitleEl.innerText = `${data["fw.username"]}`;
|
||||||
|
else if (data["fw.email"])
|
||||||
|
subtitleEl.innerText = `${data["fw.email"]}`;
|
||||||
|
|
||||||
|
if (data["fw.message"]) {
|
||||||
|
const messageEl = document.createElement('div');
|
||||||
|
messageEl.innerHTML = `<i>${data["fw.message"]}</i>`;
|
||||||
|
|
||||||
|
contentEl.appendChild(messageEl);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
contentEl.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
// case ('FourthwallGiftPurchase'):
|
||||||
|
// break;
|
||||||
|
case ('FourthwallOrderPlaced'):
|
||||||
|
{
|
||||||
|
// Only print non-free orders
|
||||||
|
if (data["fw.total"] <= 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const avatarURL = await GetAvatar(data["fw.username"], 'twitch');
|
||||||
|
if (IsValidUrl(avatarURL))
|
||||||
|
avatarEl.src = avatarURL;
|
||||||
|
else
|
||||||
|
avatarEl.style.display = 'none'
|
||||||
|
titleEl.style.fontSize = '2em';
|
||||||
|
titleEl.innerText = FormatCurrency(data["fw.total"], data["fw.currency"]);
|
||||||
|
if (data["fw.username"])
|
||||||
|
subtitleEl.innerText = `${data["fw.username"]}`;
|
||||||
|
else if (data["fw.email"])
|
||||||
|
subtitleEl.innerText = `${data["fw.email"]}`;
|
||||||
|
|
||||||
|
// Compile a list of all items bought
|
||||||
|
const variants = [];
|
||||||
|
|
||||||
|
// Iterate through all keys in the data object
|
||||||
|
for (const key in data) {
|
||||||
|
const match = key.match(/^fw\.variants\[(\d+)\]\.(\w+)$/);
|
||||||
|
if (match) {
|
||||||
|
const index = Number(match[1]);
|
||||||
|
const field = match[2];
|
||||||
|
|
||||||
|
// Make sure the array slot exists
|
||||||
|
if (!variants[index]) {
|
||||||
|
variants[index] = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assign the field to the appropriate variant object
|
||||||
|
variants[index][field] = data[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print each item on the receipt
|
||||||
|
const messageEl = document.createElement('div');
|
||||||
|
variants.forEach((variant, i) => {
|
||||||
|
messageEl.innerHTML += `${variant.quantity} × ${variant.name}<br>`;
|
||||||
|
});
|
||||||
|
messageEl.style.textAlign = 'left';
|
||||||
|
|
||||||
|
// Check if they left a custom message
|
||||||
|
let customMessageEl = document.createElement('div');
|
||||||
|
const customMessage = data["fw.statmessageus"];
|
||||||
|
if (customMessage) {
|
||||||
|
const txt = document.createElement("textarea");
|
||||||
|
txt.innerHTML = customMessage;
|
||||||
|
customMessageEl.innerHTML += `<br><i>${txt.value}</i>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a cute thank you message because you're uwu like that
|
||||||
|
const thankYouEl = document.createElement('div');
|
||||||
|
thankYouEl.innerHTML += `<br><b>Thank you for your purchase!</b>`;
|
||||||
|
|
||||||
|
contentEl.appendChild(messageEl);
|
||||||
|
contentEl.appendChild(customMessageEl);
|
||||||
|
contentEl.appendChild(thankYouEl);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case ('FourthwallSubscriptionPurchased'):
|
||||||
|
{
|
||||||
|
const avatarURL = await GetAvatar(data["fw.nickname"], 'twitch');
|
||||||
|
if (IsValidUrl(avatarURL))
|
||||||
|
avatarEl.src = avatarURL;
|
||||||
|
else
|
||||||
|
avatarEl.style.display = 'none'
|
||||||
|
titleEl.innerText = `New Member`;
|
||||||
|
subtitleEl.innerHTML = `${data["fw.nickname"]}`;
|
||||||
|
|
||||||
|
const messageEl = document.createElement('div');
|
||||||
|
messageEl.innerHTML = `Thanks for joining at the <b>${FormatCurrency(data["fw.amount"], data["fw.currency"])}</b> tier!`;
|
||||||
|
|
||||||
|
contentEl.appendChild(messageEl);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
// Custom Code Events
|
||||||
|
case ('CustomCodeEvent'):
|
||||||
|
{
|
||||||
|
switch (data.triggerCustomCodeEventName) {
|
||||||
|
case ('kickIncomingRaid'):
|
||||||
|
{
|
||||||
|
avatarEl.src = ConvertWEBPToPNG(await GetAvatar(data.user, 'kick'));
|
||||||
|
|
||||||
|
const messageEl = document.createElement('div');
|
||||||
|
messageEl.innerHTML = `<b>${data.user}</b><br>is hosting with a party of<br><b>${data.viewers} viewers!</b>`;
|
||||||
|
|
||||||
|
contentEl.appendChild(messageEl);
|
||||||
|
|
||||||
|
// Set the platform icon
|
||||||
|
SetPlatformIcon(iconEl, 'kick');
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
// Don't print any event not excplicitly listed above
|
||||||
|
default:
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the timestamp
|
||||||
|
const { DateTime } = luxon;
|
||||||
|
const now = DateTime.local();
|
||||||
|
const formatted = now.toFormat("cccc, LLLL d',' yyyy HH:mm:ss");
|
||||||
|
|
||||||
|
// Add ordinal suffix manually
|
||||||
|
function addOrdinal(n) {
|
||||||
|
if (n >= 11 && n <= 13) return 'th';
|
||||||
|
switch (n % 10) {
|
||||||
|
case 1: return 'st';
|
||||||
|
case 2: return 'nd';
|
||||||
|
case 3: return 'rd';
|
||||||
|
default: return 'th';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const day = now.day;
|
||||||
|
const ordinal = addOrdinal(day);
|
||||||
|
const fullFormatted = now.toFormat(`cccc, LLLL '${day}${ordinal}' yyyy HH:mm:ss`);
|
||||||
|
|
||||||
|
dateEl.textContent = fullFormatted;
|
||||||
|
|
||||||
|
// Send it to the print routine!
|
||||||
|
const receiptHTML = await GetRenderedHTML(instance);
|
||||||
|
|
||||||
|
window.parent.sbClient.doAction({ id: sbActionPrintRoutine }, {
|
||||||
|
receiptHTML: receiptHTML,
|
||||||
|
isTest: data.isTest,
|
||||||
|
printerName: document.getElementById('printer-name').value,
|
||||||
|
paperWidth: document.getElementById('paper-width').value,
|
||||||
|
ignoreTestTriggers: document.getElementById('ignore-test-triggers').checked,
|
||||||
|
deleteTempFiles: document.getElementById('delete-temp-files').checked
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//////////////////////
|
||||||
|
// HELPER FUNCTIONS //
|
||||||
|
//////////////////////
|
||||||
|
|
||||||
|
async function GetAvatar(username, platform) {
|
||||||
|
|
||||||
|
// First, check if the username is hashed already
|
||||||
|
if (avatarMap.has(`${username}-${platform}`)) {
|
||||||
|
console.debug(`Avatar found for ${username} (${platform}). Retrieving from hash map.`)
|
||||||
|
return avatarMap.get(`${username}-${platform}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If code reaches this point, the username hasn't been hashed, so retrieve avatar
|
||||||
|
switch (platform) {
|
||||||
|
case 'twitch':
|
||||||
|
{
|
||||||
|
console.debug(`No avatar found for ${username} (${platform}). Retrieving from Decapi.`)
|
||||||
|
let response = await fetch('https://decapi.me/twitch/avatar/' + username);
|
||||||
|
let data = await response.text();
|
||||||
|
avatarMap.set(`${username}-${platform}`, data);
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
case 'kick':
|
||||||
|
{
|
||||||
|
console.debug(`No avatar found for ${username} (${platform}). Retrieving from Kick.`)
|
||||||
|
try {
|
||||||
|
let response = await fetch('https://kick.com/api/v2/channels/' + username);
|
||||||
|
console.log('https://kick.com/api/v2/channels/' + username)
|
||||||
|
let data = await response.json();
|
||||||
|
let avatarURL = data.user.profile_pic;
|
||||||
|
if (!avatarURL)
|
||||||
|
avatarURL = 'https://kick.com/img/default-profile-pictures/default2.jpeg';
|
||||||
|
avatarMap.set(`${username}-${platform}`, avatarURL);
|
||||||
|
return avatarURL;
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.debug(error);
|
||||||
|
return 'https://kick.com/img/default-profile-pictures/default2.jpeg';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function GetRenderedHTML(fragment) {
|
||||||
|
if (!(fragment instanceof DocumentFragment)) {
|
||||||
|
throw new Error('Argument must be a DocumentFragment');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter out comment nodes from fragment content
|
||||||
|
const nodes = Array.from(fragment.childNodes).filter(
|
||||||
|
node => node.nodeType !== Node.COMMENT_NODE
|
||||||
|
);
|
||||||
|
|
||||||
|
const bodyContent = nodes
|
||||||
|
.map(node => node.outerHTML || node.textContent)
|
||||||
|
.join('');
|
||||||
|
|
||||||
|
// Get inline <style> contents
|
||||||
|
const inlineStyles = Array.from(document.querySelectorAll('style'))
|
||||||
|
.map(style => style.textContent)
|
||||||
|
.join('\n');
|
||||||
|
|
||||||
|
// Get all external stylesheet URLs
|
||||||
|
const linkHrefs = Array.from(document.querySelectorAll('link[rel="stylesheet"]'))
|
||||||
|
.map(link => link.href);
|
||||||
|
|
||||||
|
// Fetch external CSS contents
|
||||||
|
const externalCSSContents = await Promise.all(
|
||||||
|
linkHrefs.map(async href => {
|
||||||
|
try {
|
||||||
|
const res = await fetch(href);
|
||||||
|
if (!res.ok) throw new Error(`Failed to load CSS from ${href}`);
|
||||||
|
return await res.text();
|
||||||
|
} catch {
|
||||||
|
console.warn(`Could not fetch CSS from ${href}`);
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// Combine all CSS into one string
|
||||||
|
const combinedCSS = inlineStyles + '\n' + externalCSSContents.join('\n');
|
||||||
|
|
||||||
|
// Build the full standalone HTML string
|
||||||
|
const fullHTML = `
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<style>${combinedCSS}</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
${bodyContent}
|
||||||
|
</body>
|
||||||
|
</html>`.trim();
|
||||||
|
|
||||||
|
return fullHTML;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ConvertWEBPToPNG(URL) {
|
||||||
|
return `https://images.weserv.nl/?url=${URL}&output=png`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function FormatCurrency(amount, currency) {
|
||||||
|
const isISOCode = /^[A-Z]{3}$/.test(currency);
|
||||||
|
|
||||||
|
if (isISOCode) {
|
||||||
|
try {
|
||||||
|
return new Intl.NumberFormat(undefined, {
|
||||||
|
style: 'currency',
|
||||||
|
currency: currency,
|
||||||
|
currencyDisplay: 'symbol',
|
||||||
|
}).format(amount);
|
||||||
|
} catch {
|
||||||
|
return `${amount.toFixed(2)} ${currency}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle some common symbols that go before the number
|
||||||
|
const symbolsBefore = ['$', '€', '£', '¥', '₹'];
|
||||||
|
|
||||||
|
if (symbolsBefore.includes(currency)) {
|
||||||
|
return `${currency}${amount.toFixed(2)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise default to appending after
|
||||||
|
return `${amount.toFixed(2)} ${currency}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function IsValidUrl(string) {
|
||||||
|
try {
|
||||||
|
new URL(string);
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function EscapeRegExp(string) {
|
||||||
|
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
|
||||||
|
}
|
||||||
|
|
||||||
|
function SetPlatformIcon(el, platform) {
|
||||||
|
// Set the platform icon
|
||||||
|
let baseURL = window.location.href;
|
||||||
|
baseURL = baseURL.replace(/index\.html$/i, '');
|
||||||
|
|
||||||
|
el.src = `${baseURL}/icons/platforms/${platform}.png`;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
///////////////////////
|
||||||
|
// PAGE INTERACTIONS //
|
||||||
|
///////////////////////
|
||||||
|
|
||||||
|
let data = {
|
||||||
|
"__source": "TwitchSub",
|
||||||
|
"tier": "prime",
|
||||||
|
"isPrimeSub": true,
|
||||||
|
"monthsSubscribed": 1,
|
||||||
|
"isTest": false,
|
||||||
|
"actionName": "Printer Bot | Events",
|
||||||
|
"user": "nutty",
|
||||||
|
"userName": "nutty",
|
||||||
|
"userType": "twitch"
|
||||||
|
}
|
||||||
|
|
||||||
|
async function TestPrint() {
|
||||||
|
CustomEvent(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
///////////////////
|
||||||
|
// PAGE SETTINGS //
|
||||||
|
///////////////////
|
||||||
|
|
||||||
|
// Get references
|
||||||
|
const printerNameInput = document.getElementById('printer-name');
|
||||||
|
const paperWidthInput = document.getElementById('paper-width');
|
||||||
|
const ignoreTestTriggersInput = document.getElementById('ignore-test-triggers');
|
||||||
|
const deleteTempFilesInput = document.getElementById('delete-temp-files');
|
||||||
|
|
||||||
|
// Local storage key must be prefixed with the first URL segment
|
||||||
|
const currentPath = window.location.pathname;
|
||||||
|
const urlSegment = currentPath.split('/').filter(Boolean)[0];
|
||||||
|
const storageKey = (id) => `${urlSegment}::${id}`;
|
||||||
|
|
||||||
|
function saveSetting(id) {
|
||||||
|
const el = document.getElementById(id);
|
||||||
|
const value = el.type === "checkbox" ? el.checked : el.value;
|
||||||
|
localStorage.setItem(storageKey(id), value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add event listeners
|
||||||
|
[printerNameInput, paperWidthInput, ignoreTestTriggersInput, deleteTempFilesInput].forEach(input => {
|
||||||
|
input.addEventListener("input", () => saveSetting(input.id));
|
||||||
|
input.addEventListener("change", () => saveSetting(input.id));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Load settings
|
||||||
|
if (localStorage.getItem(storageKey(printerNameInput.id)))
|
||||||
|
printerNameInput.value = localStorage.getItem(storageKey(printerNameInput.id));
|
||||||
|
if (localStorage.getItem(storageKey(paperWidthInput.id)))
|
||||||
|
paperWidthInput.value = localStorage.getItem(storageKey(paperWidthInput.id));
|
||||||
|
if (localStorage.getItem(storageKey(ignoreTestTriggersInput.id)))
|
||||||
|
ignoreTestTriggersInput.checked = JSON.parse(localStorage.getItem(storageKey(ignoreTestTriggersInput.id)));
|
||||||
|
if (localStorage.getItem(storageKey(deleteTempFilesInput.id)))
|
||||||
|
deleteTempFilesInput.checked = JSON.parse(localStorage.getItem(storageKey(deleteTempFilesInput.id)));
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
body {
|
||||||
|
margin: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
#settings {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting input {
|
||||||
|
margin-left: auto;
|
||||||
|
width: 15em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/************************/
|
||||||
|
/*** RECEIPT TEMPLATE ***/
|
||||||
|
/************************/
|
||||||
|
|
||||||
|
#receipt-container {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica, Arial, sans-serif;
|
||||||
|
text-align: center;
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* #receipt-header {} */
|
||||||
|
|
||||||
|
#receipt-content {
|
||||||
|
padding: 0.5em 0em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* #receipt-footer {} */
|
||||||
|
|
||||||
|
#receipt-avatar {
|
||||||
|
max-height: 15em;
|
||||||
|
max-width: 90%;
|
||||||
|
border-radius: 50%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
#receipt-title {
|
||||||
|
font-weight: 900;
|
||||||
|
font-size: 1.5em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
#receipt-subtitle {
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 1.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
#receipt-icon {
|
||||||
|
height: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
#receipt-icon:not([src]),
|
||||||
|
#receipt-icon[src=""] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#receipt-date {
|
||||||
|
margin: 0.5em 0em;
|
||||||
|
font-size: 0.7em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.emote {
|
||||||
|
height: 1em;
|
||||||
|
}
|
||||||
@@ -1,49 +1,20 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<link rel="shortcut icon" href="#" />
|
<link rel="stylesheet" href="../.common/styles/global.css" />
|
||||||
<link rel="stylesheet" href="./style.css" />
|
<link rel="icon" href="../../.resources/logo.png" type="image/png">
|
||||||
<script type="text/javascript" src="https://unpkg.com/@streamerbot/client/dist/streamerbot-client.js"></script>
|
<title>nutty</title>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/luxon@3/build/global/luxon.min.js"></script>
|
<style>
|
||||||
|
#dock-wrapper {
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div id="background">
|
<iframe id="dock-wrapper"></iframe>
|
||||||
<div id="connect-box">
|
<script src="script.js"></script>
|
||||||
<div class="field">
|
|
||||||
<label for="ip">IP Address</label>
|
|
||||||
<input type="text" id="ip" name="ip" placeholder="127.0.0.1" value="127.0.0.1">
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<label for="port">Port</label>
|
|
||||||
<input type="text" id="port" name="port" placeholder="8080" value="8080">
|
|
||||||
</div>
|
|
||||||
<button id="connect-button" onclick="Connect()">Connect</button>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
<template id="receipt-template">
|
</html>
|
||||||
<!-- Header -->
|
|
||||||
<div id="header">
|
|
||||||
<img id="avatar">
|
|
||||||
<div>
|
|
||||||
<div id="title"></div>
|
|
||||||
<div id="subtitle"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Contents -->
|
|
||||||
<div id="content">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Footer -->
|
|
||||||
<div id="footer">
|
|
||||||
<div id="date"></div>
|
|
||||||
<img id="icon" src="icons/platforms/twitch.png">
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script src="./script.js"></script>
|
|
||||||
@@ -1,804 +1,16 @@
|
|||||||
///////////////////
|
// Construct URL
|
||||||
// PAGE ELEMENTS //
|
const currentURL = window.location.href;
|
||||||
///////////////////
|
let baseURL = currentURL;
|
||||||
|
|
||||||
const headerEl = document.getElementById('header');
|
if (baseURL.endsWith("index.html"))
|
||||||
const contentEl = document.getElementById('content');
|
baseURL = baseURL.replace("index.html", "");
|
||||||
const footerEl = document.getElementById('footer');
|
|
||||||
|
|
||||||
const avatarEl = document.getElementById('avatar');
|
const configJson = "?config=" + baseURL + "config.json";
|
||||||
const titleEl = document.getElementById('title');
|
|
||||||
const subtitleEl = document.getElementById('subtitle');
|
|
||||||
const dateEl = document.getElementById('date');
|
|
||||||
|
|
||||||
|
// Implement widget dock core
|
||||||
|
window.dockWrapper = document.getElementById('dock-wrapper');
|
||||||
|
dockWrapper.src = `../../.common/core/widget-dock-core${configJson}`;
|
||||||
|
|
||||||
|
dockWrapper.addEventListener('load', () => {
|
||||||
//////////////////////
|
dockWrapper.contentWindow.content.src = baseURL + '/contents';
|
||||||
// GLOBAL VARIABLES //
|
});
|
||||||
//////////////////////
|
|
||||||
|
|
||||||
const avatarMap = new Map();
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/////////////////////////
|
|
||||||
// STREAMER.BOT CLIENT //
|
|
||||||
/////////////////////////
|
|
||||||
|
|
||||||
// Check local storage
|
|
||||||
if (localStorage.getItem('sbServerAddress') === null)
|
|
||||||
localStorage.setItem('sbServerAddress', '127.0.0.1');
|
|
||||||
if (localStorage.getItem('sbServerPort') === null)
|
|
||||||
localStorage.setItem('sbServerPort', '8080');
|
|
||||||
|
|
||||||
document.getElementById('ip').value = localStorage.getItem('sbServerAddress');
|
|
||||||
document.getElementById('port').value = localStorage.getItem('sbServerPort');
|
|
||||||
|
|
||||||
let sbServerAddress = document.getElementById('ip').value;
|
|
||||||
let sbServerPort = document.getElementById('port').value;
|
|
||||||
|
|
||||||
let client = new StreamerbotClient({
|
|
||||||
host: sbServerAddress,
|
|
||||||
port: sbServerPort,
|
|
||||||
|
|
||||||
onConnect: (data) => {
|
|
||||||
console.log(`Streamer.bot successfully connected to ${sbServerAddress}:${sbServerPort}`)
|
|
||||||
console.debug(data);
|
|
||||||
|
|
||||||
SetConnectionState(true);
|
|
||||||
},
|
|
||||||
|
|
||||||
onDisconnect: () => {
|
|
||||||
console.error(`Streamer.bot disconnected from ${sbServerAddress}:${sbServerPort}`)
|
|
||||||
SetConnectionState(false);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
client.on('General.Custom', (response) => {
|
|
||||||
console.debug(response.data);
|
|
||||||
CustomEvent(response.data);
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
////////////////////
|
|
||||||
// STREAM PRINTER //
|
|
||||||
////////////////////
|
|
||||||
|
|
||||||
async function CustomEvent(data) {
|
|
||||||
if (data.actionName != 'Printer Bot | Events')
|
|
||||||
return;
|
|
||||||
|
|
||||||
// Get a reference to the template
|
|
||||||
const template = document.getElementById('receipt-template');
|
|
||||||
|
|
||||||
// Create a new instance of the template
|
|
||||||
const instance = template.content.cloneNode(true);
|
|
||||||
|
|
||||||
// Get divs
|
|
||||||
const headerEl = instance.querySelector('#header');
|
|
||||||
const contentEl = instance.querySelector('#content');
|
|
||||||
const footerEl = instance.querySelector('#footer');
|
|
||||||
const avatarEl = instance.querySelector('#avatar');
|
|
||||||
const titleEl = instance.querySelector('#title');
|
|
||||||
const subtitleEl = instance.querySelector('#subtitle');
|
|
||||||
const iconEl = instance.querySelector('#icon');
|
|
||||||
const dateEl = instance.querySelector('#date');
|
|
||||||
|
|
||||||
// Set the main contents
|
|
||||||
switch (data.__source) {
|
|
||||||
// Twitch events
|
|
||||||
case ('TwitchCheer'):
|
|
||||||
{
|
|
||||||
avatarEl.src = await GetAvatar(data.userName, 'twitch');
|
|
||||||
titleEl.innerText = `${data.bits} BITS`;
|
|
||||||
subtitleEl.innerText = `${data.user}`;
|
|
||||||
|
|
||||||
const messageEl = document.createElement('div');
|
|
||||||
messageEl.innerHTML = data.message;
|
|
||||||
|
|
||||||
// Render emotes
|
|
||||||
for (i in data.emotes) {
|
|
||||||
const emoteElement = `<img src="${data.emotes[i].imageUrl}" class="emote"/>`;
|
|
||||||
const emoteName = EscapeRegExp(data.emotes[i].name);
|
|
||||||
|
|
||||||
let regexPattern = emoteName;
|
|
||||||
|
|
||||||
// Check if the emote name consists only of word characters (alphanumeric and underscore)
|
|
||||||
if (/^\w+$/.test(emoteName)) {
|
|
||||||
regexPattern = `\\b${emoteName}\\b`;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// For non-word emotes, ensure they are surrounded by non-word characters or boundaries
|
|
||||||
regexPattern = `(?<=^|[^\\w])${emoteName}(?=$|[^\\w])`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const regex = new RegExp(regexPattern, 'g');
|
|
||||||
messageEl.innerHTML = messageEl.innerHTML.replace(regex, emoteElement);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Render cheermotes
|
|
||||||
for (i in data.cheerEmotes) {
|
|
||||||
const bits = data.cheerEmotes[i].bits;
|
|
||||||
const imageUrl = data.cheerEmotes[i].imageUrl;
|
|
||||||
const name = data.cheerEmotes[i].name;
|
|
||||||
const cheerEmoteElement = `<img src="${imageUrl}" class="emote"/>`;
|
|
||||||
const bitsElements = `<span class="bits">${bits}</span>`
|
|
||||||
messageEl.innerHTML = messageEl.innerHTML.replace(new RegExp(`\\b${name}${bits}\\b`, 'i'), cheerEmoteElement + bitsElements);
|
|
||||||
}
|
|
||||||
|
|
||||||
contentEl.appendChild(messageEl);
|
|
||||||
|
|
||||||
// Set the platform icon
|
|
||||||
SetPlatformIcon(iconEl, 'twitch');
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case ('TwitchSub'):
|
|
||||||
{
|
|
||||||
avatarEl.src = await GetAvatar(data.userName, 'twitch');
|
|
||||||
titleEl.innerText = `${data.tier} subscriber`;
|
|
||||||
subtitleEl.innerText = `${data.user}`;
|
|
||||||
|
|
||||||
const messageEl = document.createElement('div');
|
|
||||||
messageEl.innerHTML = '<b>First time subscriber!</b>';
|
|
||||||
|
|
||||||
contentEl.appendChild(messageEl);
|
|
||||||
|
|
||||||
// Set the platform icon
|
|
||||||
SetPlatformIcon(iconEl, 'twitch');
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case ('TwitchReSub'):
|
|
||||||
{
|
|
||||||
avatarEl.src = await GetAvatar(data.userName, 'twitch');
|
|
||||||
titleEl.innerText = `${data.tier} subscriber`;
|
|
||||||
subtitleEl.innerText = `${data.user}`;
|
|
||||||
|
|
||||||
const messageEl = document.createElement('div');
|
|
||||||
messageEl.innerHTML = `<b>${data.cumulative} months${data.monthStreak > 1 ? ' (' + data.monthStreak + ' in a row!)' : ''}</b>`;
|
|
||||||
if (data.messageStripped)
|
|
||||||
messageEl.innerHTML += `<br><br><i>${data.messageStripped}</i>`;
|
|
||||||
|
|
||||||
contentEl.appendChild(messageEl);
|
|
||||||
|
|
||||||
// Set the platform icon
|
|
||||||
SetPlatformIcon(iconEl, 'twitch');
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case ('TwitchGiftSub'):
|
|
||||||
{
|
|
||||||
// Don't put profile pictures for subs that come from Gift Bombs to avoid rate limits
|
|
||||||
if (data.fromGiftBomb)
|
|
||||||
//avatarEl.style.display = 'none';
|
|
||||||
return;
|
|
||||||
else
|
|
||||||
avatarEl.src = await GetAvatar(data.recipientUserName, 'twitch');
|
|
||||||
titleEl.innerText = `Gifted Sub`;
|
|
||||||
|
|
||||||
const messageEl = document.createElement('div');
|
|
||||||
messageEl.innerHTML += `<b>${data.recipientUser}</b><br>received a ${data.tier} sub from<br>`;
|
|
||||||
if (data.anonymous)
|
|
||||||
messageEl.innerHTML += `a mysterious admirer...`;
|
|
||||||
else
|
|
||||||
messageEl.innerHTML += `<b>${data.user}</b>!`;
|
|
||||||
|
|
||||||
contentEl.appendChild(messageEl);
|
|
||||||
|
|
||||||
// Set the platform icon
|
|
||||||
SetPlatformIcon(iconEl, 'twitch');
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case ('TwitchGiftBomb'):
|
|
||||||
{
|
|
||||||
avatarEl.src = await GetAvatar(data.userName, 'twitch');
|
|
||||||
titleEl.innerHTML = `${data.gifts} × Gifted Subs`;
|
|
||||||
subtitleEl.innerHTML += `✧*・゚✧ ${data.tier.toUpperCase()} ✧・゚*✧`;
|
|
||||||
if (data.anonymous)
|
|
||||||
subtitleEl.innerHTML += `<br>From a mystery person...`;
|
|
||||||
else
|
|
||||||
subtitleEl.innerHTML += `<br>${data.user}`;
|
|
||||||
|
|
||||||
const messageEl = document.createElement('div');
|
|
||||||
if (data.totalGifts > 1) {
|
|
||||||
messageEl.innerHTML = `They've gifted <b>${data.totalGifts} subs</b> in total!</br></br>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get a list of all recipient users
|
|
||||||
Object.keys(data)
|
|
||||||
.filter(key => /^gift\.recipientUser\d+$/.test(key))
|
|
||||||
.forEach((key, index) => {
|
|
||||||
const username = data[key];
|
|
||||||
messageEl.innerHTML += `${username}</br>`;
|
|
||||||
});
|
|
||||||
|
|
||||||
contentEl.appendChild(messageEl);
|
|
||||||
|
|
||||||
// Set the platform icon
|
|
||||||
SetPlatformIcon(iconEl, 'twitch');
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case ('TwitchRaid'):
|
|
||||||
{
|
|
||||||
avatarEl.src = await GetAvatar(data.userName, 'twitch');
|
|
||||||
|
|
||||||
const messageEl = document.createElement('div');
|
|
||||||
messageEl.innerHTML = `<b>${data.user}</b><br>is raiding with a party of<br><b>${data.viewers} viewers!</b>`;
|
|
||||||
|
|
||||||
contentEl.appendChild(messageEl);
|
|
||||||
|
|
||||||
// Set the platform icon
|
|
||||||
SetPlatformIcon(iconEl, 'twitch');
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
// YouTube Events
|
|
||||||
case ('YouTubeNewSponsor'):
|
|
||||||
{
|
|
||||||
if (data.userProfileUrl)
|
|
||||||
avatarEl.src = data.userProfileUrl;
|
|
||||||
else
|
|
||||||
avatarEl.style.display = 'none';
|
|
||||||
|
|
||||||
titleEl.innerText = `${data.levelName}`;
|
|
||||||
subtitleEl.innerText = `${data.user}`;
|
|
||||||
|
|
||||||
contentEl.style.display = 'none';
|
|
||||||
|
|
||||||
// Set the platform icon
|
|
||||||
SetPlatformIcon(iconEl, 'youtube');
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case ('YouTubeGiftMembershipReceived'):
|
|
||||||
{
|
|
||||||
if (data.gifterProfileUrl)
|
|
||||||
avatarEl.src = data.gifterProfileUrl;
|
|
||||||
else
|
|
||||||
avatarEl.style.display = 'none';
|
|
||||||
titleEl.innerText = `Gifted Membership`;
|
|
||||||
|
|
||||||
const messageEl = document.createElement('div');
|
|
||||||
messageEl.innerHTML = `<b>${data.user}</b><br>received a membership from<br><b>${data.gifterUser}</b>!`;
|
|
||||||
|
|
||||||
contentEl.appendChild(messageEl);
|
|
||||||
|
|
||||||
// Set the platform icon
|
|
||||||
SetPlatformIcon(iconEl, 'youtube');
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case ('YouTubeSuperChat'):
|
|
||||||
{
|
|
||||||
if (data.userProfileUrl)
|
|
||||||
avatarEl.src = data.userProfileUrl;
|
|
||||||
else
|
|
||||||
avatarEl.style.display = 'none';
|
|
||||||
titleEl.style.fontSize = '2em';
|
|
||||||
titleEl.innerText = `${data.amount}`;
|
|
||||||
|
|
||||||
const messageEl = document.createElement('div');
|
|
||||||
messageEl.innerHTML = `<b>${data.user}</b><br>sent a Super Chat!`;
|
|
||||||
if (data.message)
|
|
||||||
messageEl.innerHTML += `<br><br><i>${data.message}</i>`;
|
|
||||||
|
|
||||||
contentEl.appendChild(messageEl);
|
|
||||||
|
|
||||||
// Set the platform icon
|
|
||||||
SetPlatformIcon(iconEl, 'youtube');
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case ('YouTubeSuperSticker'):
|
|
||||||
{
|
|
||||||
if (data.stickerImageUrl)
|
|
||||||
avatarEl.src = data.stickerImageUrl;
|
|
||||||
else
|
|
||||||
avatarEl.style.display = 'none';
|
|
||||||
titleEl.style.fontSize = '2em';
|
|
||||||
titleEl.innerText = `${data.amount}`;
|
|
||||||
|
|
||||||
const messageEl = document.createElement('div');
|
|
||||||
messageEl.innerHTML = `<b>${data.user}</b><br>sent a Super Sticker!`;
|
|
||||||
|
|
||||||
contentEl.appendChild(messageEl);
|
|
||||||
|
|
||||||
// Set the platform icon
|
|
||||||
SetPlatformIcon(iconEl, 'youtube');
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
break;
|
|
||||||
|
|
||||||
// Kick Events
|
|
||||||
case ('KickSubscription'):
|
|
||||||
case ('KickResubscription'):
|
|
||||||
{
|
|
||||||
avatarEl.src = ConvertWEBPToPNG(await GetAvatar(data.user, 'kick'));
|
|
||||||
titleEl.innerText = `Subscriber`;
|
|
||||||
subtitleEl.innerText = `${data.user}`;
|
|
||||||
|
|
||||||
const messageEl = document.createElement('div');
|
|
||||||
if (data.duration > 1)
|
|
||||||
messageEl.innerHTML = `<b>${data.duration} months</b>`;
|
|
||||||
else
|
|
||||||
messageEl.innerHTML = '<b>First time subscriber!</b>';
|
|
||||||
|
|
||||||
contentEl.appendChild(messageEl);
|
|
||||||
|
|
||||||
// Set the platform icon
|
|
||||||
SetPlatformIcon(iconEl, 'kick');
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case ('KickGiftSubscription'):
|
|
||||||
{
|
|
||||||
avatarEl.src = ConvertWEBPToPNG(await GetAvatar(data["recipient.userLogin"], 'kick'));
|
|
||||||
titleEl.innerText = `Gifted Sub`;
|
|
||||||
|
|
||||||
const messageEl = document.createElement('div');
|
|
||||||
messageEl.innerHTML = `<b>${data["recipient.userName"]}</b><br>received a sub from<br><b>${data.user}</b>!`;
|
|
||||||
|
|
||||||
contentEl.appendChild(messageEl);
|
|
||||||
|
|
||||||
// Set the platform icon
|
|
||||||
SetPlatformIcon(iconEl, 'kick');
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case ('KickMassGiftSubscription'):
|
|
||||||
{
|
|
||||||
// There is only one sub, so use the same template for a single gifted sub
|
|
||||||
if ('recipient.userName' in data) {
|
|
||||||
avatarEl.src = ConvertWEBPToPNG(await GetAvatar(data["recipient.userLogin"], 'kick'));
|
|
||||||
titleEl.innerText = `Gifted Sub`;
|
|
||||||
|
|
||||||
const messageEl = document.createElement('div');
|
|
||||||
messageEl.innerHTML = `<b>${data["recipient.userName"]}</b><br>received a sub from<br><b>${data.user}</b>!`;
|
|
||||||
|
|
||||||
contentEl.appendChild(messageEl);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
avatarEl.src = ConvertWEBPToPNG(await GetAvatar(data.user, 'kick'));
|
|
||||||
|
|
||||||
// Calculate how many subs were gived
|
|
||||||
let maxIndex = -1;
|
|
||||||
for (const key in data) {
|
|
||||||
const match = key.match(/^recipient\.(\d+)\./);
|
|
||||||
if (match) {
|
|
||||||
const index = parseInt(match[1], 10);
|
|
||||||
if (index > maxIndex) {
|
|
||||||
maxIndex = index;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const totalGifts = maxIndex + 1;
|
|
||||||
|
|
||||||
titleEl.innerHTML = `${totalGifts} × Gifted Subs`;
|
|
||||||
subtitleEl.innerText = `${data.user}`;
|
|
||||||
|
|
||||||
const messageEl = document.createElement('div');
|
|
||||||
|
|
||||||
// Loop through each recipient and include it in the receipt
|
|
||||||
const recipients = {};
|
|
||||||
|
|
||||||
// Reconstruct recipient objects
|
|
||||||
for (const key in data) {
|
|
||||||
const match = key.match(/^recipient\.(\d+)\.(.+)$/);
|
|
||||||
if (match) {
|
|
||||||
const index = match[1];
|
|
||||||
const field = match[2];
|
|
||||||
|
|
||||||
if (!recipients[index]) {
|
|
||||||
recipients[index] = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
recipients[index][field] = data[key];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Loop through and print userName
|
|
||||||
for (const index in recipients) {
|
|
||||||
messageEl.innerHTML += `${recipients[index].userName}<br>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
contentEl.appendChild(messageEl);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set the platform icon
|
|
||||||
SetPlatformIcon(iconEl, 'kick');
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
// StreamElements Events
|
|
||||||
case ('StreamElementsTip'):
|
|
||||||
{
|
|
||||||
const avatarURL = await GetAvatar(data.tipUsername, 'twitch');
|
|
||||||
if (IsValidUrl(avatarURL))
|
|
||||||
avatarEl.src = avatarURL;
|
|
||||||
else
|
|
||||||
avatarEl.style.display = 'none'
|
|
||||||
titleEl.style.fontSize = '2em';
|
|
||||||
titleEl.innerText = FormatCurrency(data.tipAmount, data.tipCurrency);
|
|
||||||
subtitleEl.innerText = `${data.tipUsername}`;
|
|
||||||
|
|
||||||
if (data.tipMessage) {
|
|
||||||
const messageEl = document.createElement('div');
|
|
||||||
messageEl.innerHTML = `<i>${data.tipMessage}</i>`;
|
|
||||||
|
|
||||||
contentEl.appendChild(messageEl);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
contentEl.style.display = 'none';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
// Streamlabs Events
|
|
||||||
case ('StreamlabsDonation'):
|
|
||||||
{
|
|
||||||
const avatarURL = await GetAvatar(data.donationFrom, 'twitch');
|
|
||||||
if (IsValidUrl(avatarURL))
|
|
||||||
avatarEl.src = avatarURL;
|
|
||||||
else
|
|
||||||
avatarEl.style.display = 'none'
|
|
||||||
titleEl.style.fontSize = '2em';
|
|
||||||
titleEl.innerText = data.donationFormattedAmount;
|
|
||||||
subtitleEl.innerText = `${data.donationFrom}`;
|
|
||||||
|
|
||||||
if (data.donationMessage) {
|
|
||||||
const messageEl = document.createElement('div');
|
|
||||||
messageEl.innerHTML = `<i>${data.donationMessage}</i>`;
|
|
||||||
|
|
||||||
contentEl.appendChild(messageEl);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
contentEl.style.display = 'none';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
// Fourthwall Events
|
|
||||||
case ('FourthwallDonation'):
|
|
||||||
{
|
|
||||||
const avatarURL = await GetAvatar(data["fw.username"], 'twitch');
|
|
||||||
if (IsValidUrl(avatarURL))
|
|
||||||
avatarEl.src = avatarURL;
|
|
||||||
else
|
|
||||||
avatarEl.style.display = 'none'
|
|
||||||
titleEl.style.fontSize = '2em';
|
|
||||||
titleEl.innerText = FormatCurrency(data["fw.amount"], data["fw.currency"]);
|
|
||||||
if (data["fw.username"])
|
|
||||||
subtitleEl.innerText = `${data["fw.username"]}`;
|
|
||||||
else if (data["fw.email"])
|
|
||||||
subtitleEl.innerText = `${data["fw.email"]}`;
|
|
||||||
|
|
||||||
if (data["fw.message"]) {
|
|
||||||
const messageEl = document.createElement('div');
|
|
||||||
messageEl.innerHTML = `<i>${data["fw.message"]}</i>`;
|
|
||||||
|
|
||||||
contentEl.appendChild(messageEl);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
contentEl.style.display = 'none';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
// case ('FourthwallGiftPurchase'):
|
|
||||||
// break;
|
|
||||||
case ('FourthwallOrderPlaced'):
|
|
||||||
{
|
|
||||||
// Only print non-free orders
|
|
||||||
if (data["fw.total"] <= 0)
|
|
||||||
return;
|
|
||||||
|
|
||||||
const avatarURL = await GetAvatar(data["fw.username"], 'twitch');
|
|
||||||
if (IsValidUrl(avatarURL))
|
|
||||||
avatarEl.src = avatarURL;
|
|
||||||
else
|
|
||||||
avatarEl.style.display = 'none'
|
|
||||||
titleEl.style.fontSize = '2em';
|
|
||||||
titleEl.innerText = FormatCurrency(data["fw.total"], data["fw.currency"]);
|
|
||||||
if (data["fw.username"])
|
|
||||||
subtitleEl.innerText = `${data["fw.username"]}`;
|
|
||||||
else if (data["fw.email"])
|
|
||||||
subtitleEl.innerText = `${data["fw.email"]}`;
|
|
||||||
|
|
||||||
// Compile a list of all items bought
|
|
||||||
const variants = [];
|
|
||||||
|
|
||||||
// Iterate through all keys in the data object
|
|
||||||
for (const key in data) {
|
|
||||||
const match = key.match(/^fw\.variants\[(\d+)\]\.(\w+)$/);
|
|
||||||
if (match) {
|
|
||||||
const index = Number(match[1]);
|
|
||||||
const field = match[2];
|
|
||||||
|
|
||||||
// Make sure the array slot exists
|
|
||||||
if (!variants[index]) {
|
|
||||||
variants[index] = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Assign the field to the appropriate variant object
|
|
||||||
variants[index][field] = data[key];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Print each item on the receipt
|
|
||||||
const messageEl = document.createElement('div');
|
|
||||||
variants.forEach((variant, i) => {
|
|
||||||
messageEl.innerHTML += `${variant.quantity} × ${variant.name}<br>`;
|
|
||||||
});
|
|
||||||
messageEl.style.textAlign = 'left';
|
|
||||||
|
|
||||||
// Check if they left a custom message
|
|
||||||
let customMessageEl = document.createElement('div');
|
|
||||||
const customMessage = data["fw.statmessageus"];
|
|
||||||
if (customMessage)
|
|
||||||
{
|
|
||||||
const txt = document.createElement("textarea");
|
|
||||||
txt.innerHTML = customMessage;
|
|
||||||
customMessageEl.innerHTML += `<br><i>${txt.value}</i>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add a cute thank you message because you're uwu like that
|
|
||||||
const thankYouEl = document.createElement('div');
|
|
||||||
thankYouEl.innerHTML += `<br><b>Thank you for your purchase!</b>`;
|
|
||||||
|
|
||||||
contentEl.appendChild(messageEl);
|
|
||||||
contentEl.appendChild(customMessageEl);
|
|
||||||
contentEl.appendChild(thankYouEl);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case ('FourthwallSubscriptionPurchased'):
|
|
||||||
{
|
|
||||||
const avatarURL = await GetAvatar(data["fw.nickname"], 'twitch');
|
|
||||||
if (IsValidUrl(avatarURL))
|
|
||||||
avatarEl.src = avatarURL;
|
|
||||||
else
|
|
||||||
avatarEl.style.display = 'none'
|
|
||||||
titleEl.innerText = `New Member`;
|
|
||||||
subtitleEl.innerHTML = `${data["fw.nickname"]}`;
|
|
||||||
|
|
||||||
const messageEl = document.createElement('div');
|
|
||||||
messageEl.innerHTML = `Thanks for joining at the <b>${FormatCurrency(data["fw.amount"], data["fw.currency"])}</b> tier!`;
|
|
||||||
|
|
||||||
contentEl.appendChild(messageEl);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
// Custom Code Events
|
|
||||||
case ('CustomCodeEvent'):
|
|
||||||
{
|
|
||||||
switch (data.triggerCustomCodeEventName) {
|
|
||||||
case ('kickIncomingRaid'):
|
|
||||||
{
|
|
||||||
avatarEl.src = ConvertWEBPToPNG(await GetAvatar(data.user, 'kick'));
|
|
||||||
|
|
||||||
const messageEl = document.createElement('div');
|
|
||||||
messageEl.innerHTML = `<b>${data.user}</b><br>is hosting with a party of<br><b>${data.viewers} viewers!</b>`;
|
|
||||||
|
|
||||||
contentEl.appendChild(messageEl);
|
|
||||||
|
|
||||||
// Set the platform icon
|
|
||||||
SetPlatformIcon(iconEl, 'kick');
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
// Don't print any event not excplicitly listed above
|
|
||||||
default:
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set the timestamp
|
|
||||||
const { DateTime } = luxon;
|
|
||||||
const now = DateTime.local();
|
|
||||||
const formatted = now.toFormat("cccc, LLLL d',' yyyy HH:mm:ss");
|
|
||||||
|
|
||||||
// Add ordinal suffix manually
|
|
||||||
function addOrdinal(n) {
|
|
||||||
if (n >= 11 && n <= 13) return 'th';
|
|
||||||
switch (n % 10) {
|
|
||||||
case 1: return 'st';
|
|
||||||
case 2: return 'nd';
|
|
||||||
case 3: return 'rd';
|
|
||||||
default: return 'th';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const day = now.day;
|
|
||||||
const ordinal = addOrdinal(day);
|
|
||||||
const fullFormatted = now.toFormat(`cccc, LLLL '${day}${ordinal}' yyyy HH:mm:ss`);
|
|
||||||
|
|
||||||
dateEl.textContent = fullFormatted;
|
|
||||||
|
|
||||||
// Send it to the print routine!
|
|
||||||
const receiptHTML = await GetRenderedHTML(instance);
|
|
||||||
console.log(receiptHTML);
|
|
||||||
client.doAction({ name: 'Printer Bot | Print Routine' }, {
|
|
||||||
receiptHTML: receiptHTML,
|
|
||||||
isTest: data.isTest
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
//////////////////////
|
|
||||||
// HELPER FUNCTIONS //
|
|
||||||
//////////////////////
|
|
||||||
|
|
||||||
async function GetAvatar(username, platform) {
|
|
||||||
|
|
||||||
// First, check if the username is hashed already
|
|
||||||
if (avatarMap.has(`${username}-${platform}`)) {
|
|
||||||
console.debug(`Avatar found for ${username} (${platform}). Retrieving from hash map.`)
|
|
||||||
return avatarMap.get(`${username}-${platform}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// If code reaches this point, the username hasn't been hashed, so retrieve avatar
|
|
||||||
switch (platform) {
|
|
||||||
case 'twitch':
|
|
||||||
{
|
|
||||||
console.debug(`No avatar found for ${username} (${platform}). Retrieving from Decapi.`)
|
|
||||||
let response = await fetch('https://decapi.me/twitch/avatar/' + username);
|
|
||||||
let data = await response.text();
|
|
||||||
avatarMap.set(`${username}-${platform}`, data);
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
case 'kick':
|
|
||||||
{
|
|
||||||
console.debug(`No avatar found for ${username} (${platform}). Retrieving from Kick.`)
|
|
||||||
try {
|
|
||||||
let response = await fetch('https://kick.com/api/v2/channels/' + username);
|
|
||||||
console.log('https://kick.com/api/v2/channels/' + username)
|
|
||||||
let data = await response.json();
|
|
||||||
let avatarURL = data.user.profile_pic;
|
|
||||||
if (!avatarURL)
|
|
||||||
avatarURL = 'https://kick.com/img/default-profile-pictures/default2.jpeg';
|
|
||||||
avatarMap.set(`${username}-${platform}`, avatarURL);
|
|
||||||
return avatarURL;
|
|
||||||
}
|
|
||||||
catch (error) {
|
|
||||||
console.debug(error);
|
|
||||||
return 'https://kick.com/img/default-profile-pictures/default2.jpeg';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function GetRenderedHTML(fragment) {
|
|
||||||
if (!(fragment instanceof DocumentFragment)) {
|
|
||||||
throw new Error('Argument must be a DocumentFragment');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filter out comment nodes from fragment content
|
|
||||||
const nodes = Array.from(fragment.childNodes).filter(
|
|
||||||
node => node.nodeType !== Node.COMMENT_NODE
|
|
||||||
);
|
|
||||||
|
|
||||||
const bodyContent = nodes
|
|
||||||
.map(node => node.outerHTML || node.textContent)
|
|
||||||
.join('');
|
|
||||||
|
|
||||||
// Get inline <style> contents
|
|
||||||
const inlineStyles = Array.from(document.querySelectorAll('style'))
|
|
||||||
.map(style => style.textContent)
|
|
||||||
.join('\n');
|
|
||||||
|
|
||||||
// Get all external stylesheet URLs
|
|
||||||
const linkHrefs = Array.from(document.querySelectorAll('link[rel="stylesheet"]'))
|
|
||||||
.map(link => link.href);
|
|
||||||
|
|
||||||
// Fetch external CSS contents
|
|
||||||
const externalCSSContents = await Promise.all(
|
|
||||||
linkHrefs.map(async href => {
|
|
||||||
try {
|
|
||||||
const res = await fetch(href);
|
|
||||||
if (!res.ok) throw new Error(`Failed to load CSS from ${href}`);
|
|
||||||
return await res.text();
|
|
||||||
} catch {
|
|
||||||
console.warn(`Could not fetch CSS from ${href}`);
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
// Combine all CSS into one string
|
|
||||||
const combinedCSS = inlineStyles + '\n' + externalCSSContents.join('\n');
|
|
||||||
|
|
||||||
// Build the full standalone HTML string
|
|
||||||
const fullHTML = `
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<title>Exported Page</title>
|
|
||||||
<style>${combinedCSS}</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
${bodyContent}
|
|
||||||
</body>
|
|
||||||
</html>`.trim();
|
|
||||||
|
|
||||||
return fullHTML;
|
|
||||||
}
|
|
||||||
|
|
||||||
function ConvertWEBPToPNG(URL) {
|
|
||||||
return `https://images.weserv.nl/?url=${URL}&output=png`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function FormatCurrency(amount, currency) {
|
|
||||||
const isISOCode = /^[A-Z]{3}$/.test(currency);
|
|
||||||
|
|
||||||
if (isISOCode) {
|
|
||||||
try {
|
|
||||||
return new Intl.NumberFormat(undefined, {
|
|
||||||
style: 'currency',
|
|
||||||
currency: currency,
|
|
||||||
currencyDisplay: 'symbol',
|
|
||||||
}).format(amount);
|
|
||||||
} catch {
|
|
||||||
return `${amount.toFixed(2)} ${currency}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle some common symbols that go before the number
|
|
||||||
const symbolsBefore = ['$', '€', '£', '¥', '₹'];
|
|
||||||
|
|
||||||
if (symbolsBefore.includes(currency)) {
|
|
||||||
return `${currency}${amount.toFixed(2)}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Otherwise default to appending after
|
|
||||||
return `${amount.toFixed(2)} ${currency}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function IsValidUrl(string) {
|
|
||||||
try {
|
|
||||||
new URL(string);
|
|
||||||
return true;
|
|
||||||
} catch {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function EscapeRegExp(string) {
|
|
||||||
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
|
|
||||||
}
|
|
||||||
|
|
||||||
function SetPlatformIcon(el, platform) {
|
|
||||||
// Set the platform icon
|
|
||||||
let baseURL = window.location.href;
|
|
||||||
baseURL = baseURL.replace(/index\.html$/i, '');
|
|
||||||
|
|
||||||
el.src = `${baseURL}/icons/platforms/${platform}.png`;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
///////////////////////////////////
|
|
||||||
// STREAMER.BOT WEBSOCKET STATUS //
|
|
||||||
///////////////////////////////////
|
|
||||||
|
|
||||||
function SetConnectionState(isConnected) {
|
|
||||||
if (isConnected) {
|
|
||||||
document.getElementById('ip').disabled = true;
|
|
||||||
document.getElementById('port').disabled = true;
|
|
||||||
document.getElementById('connect-button').style.backgroundColor = '#e43b3b';
|
|
||||||
document.getElementById('connect-button').innerText = 'Disconnect';
|
|
||||||
|
|
||||||
localStorage.setItem('sbServerAddress', document.getElementById('ip').value);
|
|
||||||
localStorage.setItem('sbServerPort', document.getElementById('port').value);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
document.getElementById('ip').disabled = false;
|
|
||||||
document.getElementById('port').disabled = false;
|
|
||||||
document.getElementById('connect-button').style.backgroundColor = '#3be477';
|
|
||||||
document.getElementById('connect-button').innerText = 'Connect';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function Connect() {
|
|
||||||
if (document.getElementById('ip').disabled)
|
|
||||||
client.disconnect();
|
|
||||||
else
|
|
||||||
{
|
|
||||||
client.options.host = document.getElementById('ip').value;
|
|
||||||
client.options.port = document.getElementById('port').value;
|
|
||||||
client.connect();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,127 +0,0 @@
|
|||||||
body {
|
|
||||||
font-size: 16px;
|
|
||||||
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica, Arial, sans-serif;
|
|
||||||
text-align: center;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#background {
|
|
||||||
color: white;
|
|
||||||
background-color: #282828;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
height: 100vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
label {
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
font-size: 1em;
|
|
||||||
font-weight: 500;
|
|
||||||
background-color: #3be477;
|
|
||||||
color: white;
|
|
||||||
opacity: 0.8;
|
|
||||||
border-width: 0;
|
|
||||||
border-radius: 0.25em;
|
|
||||||
padding: 0.25em 0.5em;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
button:hover {
|
|
||||||
opacity: 1;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
button:disabled {
|
|
||||||
opacity: 0.5;
|
|
||||||
cursor: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
input {
|
|
||||||
border-radius: 0.5em;
|
|
||||||
width: calc(100% - 20px);
|
|
||||||
margin: 10px 0px;
|
|
||||||
padding: 10px 10px;
|
|
||||||
background-color: #ffffff05;
|
|
||||||
border-width: 0px;
|
|
||||||
color: white;
|
|
||||||
font-size: 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
input:disabled {
|
|
||||||
opacity: 0.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
textarea:focus,
|
|
||||||
input:focus {
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
#connect-box {
|
|
||||||
font-size: 24px;
|
|
||||||
background-color: #181818;
|
|
||||||
text-align: left;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 1em;
|
|
||||||
padding: 2em;
|
|
||||||
border-radius: 0.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.field {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
#header {}
|
|
||||||
|
|
||||||
#avatar {
|
|
||||||
width: 6em;
|
|
||||||
height: 6em;
|
|
||||||
border-radius: 50%;
|
|
||||||
object-fit: cover;
|
|
||||||
}
|
|
||||||
|
|
||||||
#title {
|
|
||||||
font-weight: 900;
|
|
||||||
font-size: 1.5em;
|
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
|
||||||
|
|
||||||
#subtitle {
|
|
||||||
font-weight: 700;
|
|
||||||
font-size: 1.2em;
|
|
||||||
}
|
|
||||||
|
|
||||||
#attribute {
|
|
||||||
padding: 0.5em 0em;
|
|
||||||
}
|
|
||||||
|
|
||||||
#content {
|
|
||||||
/* border-top: 1px solid black;
|
|
||||||
border-bottom: 1px solid black;
|
|
||||||
border-left: none;
|
|
||||||
border-right: none;
|
|
||||||
margin: 1em 0em; */
|
|
||||||
padding: 0.5em 0em;
|
|
||||||
}
|
|
||||||
|
|
||||||
#icon {
|
|
||||||
height: 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
#date {
|
|
||||||
margin: 0.5em 0em;
|
|
||||||
font-size: 0.7em;
|
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
|
||||||
|
|
||||||
#footer {}
|
|
||||||
|
|
||||||
.emote {
|
|
||||||
height: 1em;
|
|
||||||
}
|
|
||||||