diff --git a/multistream-title-updater/connected.svg b/.common/core/widget-dock-core/icons/connected.svg similarity index 100% rename from multistream-title-updater/connected.svg rename to .common/core/widget-dock-core/icons/connected.svg diff --git a/multistream-title-updater/disconnected.svg b/.common/core/widget-dock-core/icons/disconnected.svg similarity index 100% rename from multistream-title-updater/disconnected.svg rename to .common/core/widget-dock-core/icons/disconnected.svg diff --git a/.common/core/widget-dock-core/index.html b/.common/core/widget-dock-core/index.html new file mode 100644 index 0000000..85038fd --- /dev/null +++ b/.common/core/widget-dock-core/index.html @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + + + +
+
+ + + +
+ + +
+ + +
+ + +
+ + + +
+ + +
+ +
+
+ + +
+
+ + +
+
+
+ + +
+ + + + +
+ + + \ No newline at end of file diff --git a/.common/core/widget-dock-core/script.js b/.common/core/widget-dock-core/script.js new file mode 100644 index 0000000..bdbbff8 --- /dev/null +++ b/.common/core/widget-dock-core/script.js @@ -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 + ); +} \ No newline at end of file diff --git a/.common/core/widget-dock-core/style.css b/.common/core/widget-dock-core/style.css new file mode 100644 index 0000000..a9f0f03 --- /dev/null +++ b/.common/core/widget-dock-core/style.css @@ -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; +} \ No newline at end of file diff --git a/.common/resources/logo.png b/.common/resources/logo.png new file mode 100644 index 0000000..0d8a815 Binary files /dev/null and b/.common/resources/logo.png differ diff --git a/.common/resources/streamer.bot.png b/.common/resources/streamer.bot.png new file mode 100644 index 0000000..f084c45 Binary files /dev/null and b/.common/resources/streamer.bot.png differ diff --git a/.common/styles/global.css b/.common/styles/global.css new file mode 100644 index 0000000..988fc25 --- /dev/null +++ b/.common/styles/global.css @@ -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; +} \ No newline at end of file diff --git a/.old/multistream-title-updater/connected.svg b/.old/multistream-title-updater/connected.svg new file mode 100644 index 0000000..4e0799a --- /dev/null +++ b/.old/multistream-title-updater/connected.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/.old/multistream-title-updater/disconnected.svg b/.old/multistream-title-updater/disconnected.svg new file mode 100644 index 0000000..962f8ef --- /dev/null +++ b/.old/multistream-title-updater/disconnected.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/multistream-title-updater/icons.afdesign b/.old/multistream-title-updater/icons.afdesign similarity index 100% rename from multistream-title-updater/icons.afdesign rename to .old/multistream-title-updater/icons.afdesign diff --git a/.old/multistream-title-updater/index.html b/.old/multistream-title-updater/index.html new file mode 100644 index 0000000..4c088ec --- /dev/null +++ b/.old/multistream-title-updater/index.html @@ -0,0 +1,81 @@ + + + + + + + + + + +
Refreshed
+ +
+ + +
+ + +
+ +
+
Update Titles
+ + +
+ +
+ +
+
+

+
+ + +
+
+ +
+ +
+
+ + + +
+ + + +
+ + + + \ No newline at end of file diff --git a/multistream-title-updater/info.svg b/.old/multistream-title-updater/info.svg similarity index 100% rename from multistream-title-updater/info.svg rename to .old/multistream-title-updater/info.svg diff --git a/multistream-title-updater/requiredActions.txt b/.old/multistream-title-updater/requiredActions.txt similarity index 98% rename from multistream-title-updater/requiredActions.txt rename to .old/multistream-title-updater/requiredActions.txt index 503a772..c43224d 100644 --- a/multistream-title-updater/requiredActions.txt +++ b/.old/multistream-title-updater/requiredActions.txt @@ -1,4 +1,4 @@ -[NUT] Multistream Title Updater | Fetch Broadcasts -[NUT] Multistream Title Updater | Update All Broadcasts -[NUT] Multistream Title Updater | Update Twitch Title +[NUT] Multistream Title Updater | Fetch Broadcasts +[NUT] Multistream Title Updater | Update All Broadcasts +[NUT] Multistream Title Updater | Update Twitch Title [NUT] Multistream Title Updater | Update YouTube Title \ No newline at end of file diff --git a/.old/multistream-title-updater/script.js b/.old/multistream-title-updater/script.js new file mode 100644 index 0000000..72ec198 --- /dev/null +++ b/.old/multistream-title-updater/script.js @@ -0,0 +1,444 @@ +//////////// +// FIELDS // +//////////// + +let sbDebugMode = true; + +const queryString = window.location.search; +const urlParams = new URLSearchParams(queryString); + +const sbServerPort = urlParams.get("port") || 8080; +const sbServerAddress = urlParams.get("server") || "127.0.0.1"; + +///////////////// +// GLOBAL VARS // +///////////////// + +let ws; + +/////////////////////////////////// +// SRTEAMER.BOT WEBSOCKET SERVER // +/////////////////////////////////// + +// This is the main function that connects to the Streamer.bot websocke server +function connectws() { + if ("WebSocket" in window) { + ws = new WebSocket("ws://" + sbServerAddress + ":" + sbServerPort + "/"); + + // Reconnect + ws.onclose = function () { + SetConnectionStatus(false); + setTimeout(connectws, 5000); + }; + + // Connect + ws.onopen = async function () { + SetConnectionStatus(true); + + console.log("Subscribe to events"); + ws.send( + JSON.stringify({ + request: "Subscribe", + id: "subscribe-events-id", + // This is the list of Streamer.bot websocket events to subscribe to + // See full list of events here: + // https://docs.streamer.bot/api/servers/websocket/requests + events: { + twitch: [ + "StreamUpdate" + ], + youTube: [ + "BroadcastStarted", + "BroadcastEnded", + "BroadcastUpdated" + ], + general: [ + "Custom" + ] + } + }) + ); + + sbGetActions(ws); + + ws.onmessage = function (event) { + // Grab message and parse JSON + const msg = event.data; + const wsdata = JSON.parse(msg); + + // Check if the user installed all the required Streamer.bot actions + if (wsdata.id == "GetActions") { + + // Check if all the required SB action exist + ReadLinesFromFile('requiredActions.txt') + .then(requiredActions => { + if (sbCheckRequiredActions(wsdata.actions, requiredActions)) { + SetElementVisibility("missingActionsInstructions", false); + sbFetchBroadcasts(ws); + } + else + SetElementVisibility("missingActionsInstructions", true); + }) + } + + if (typeof wsdata.event == "undefined") { + return; + } + + // Print data to log for debugging purposes + if (sbDebugMode) { + console.log(wsdata.data); + console.log(wsdata.event.source); + console.log(wsdata.event.type); + } + + // Check for events to trigger + // See documentation for all events here: + // https://wiki.streamer.bot/en/Servers-Clients/WebSocket-Server/Events + switch (wsdata.event.source) { + // Twitch Events + case 'Twitch': + switch (wsdata.event.type) { + case ('StreamUpdate'): + sbFetchBroadcasts(ws); + break; + } + // Twitch Events + case 'YouTube': + switch (wsdata.event.type) { + case ('BroadcastStarted'): + case ('BroadcastEnded'): + case ('BroadcastUpdated'): + sbFetchBroadcasts(ws); + break; + } + // General Events + case 'General': + switch (wsdata.event.type) { + case ('Custom'): + switch (wsdata.data.action) { + case ('[NUT] Multistream Title Updater | Fetch Broadcasts'): + UpdateBroadcastList(wsdata.data); + break; + } + break; + } + break; + + } + }; + } + } +} + + + +///////////////////////// +// STREAMER.BOT WIDGET // +///////////////////////// + +function sbGetActions(ws) { + + let request = JSON.stringify({ + request: "GetActions", + id: "GetActions" + }); + + ws.send(request); +} + +// Check if all the entries in targetActionNames exist in actionList +function sbCheckRequiredActions(actionList, targetActionNames) { + let foundActions = 0; + for (targetActionName of targetActionNames) { + if (actionList.some(action => action.name === targetActionName)) + foundActions++; + } + + return foundActions == targetActionNames.length +} + +function sbFetchBroadcasts(ws) { + + let request = JSON.stringify({ + request: "DoAction", + id: generateUUID(), + action: { + name: "[NUT] Multistream Title Updater | Fetch Broadcasts" + } + }); + + ws.send(request); +} + +function sbUpdateTitles(ws, title) { + let request = JSON.stringify({ + request: "DoAction", + id: generateUUID(), + action: { + name: "[NUT] Multistream Title Updater | Update All Broadcasts" + }, + args: { + title: title + } + + }); + + ws.send(request); +} + +function sbUpdateTwitchTitle(ws, title) { + let request = JSON.stringify({ + request: "DoAction", + id: generateUUID(), + action: { + name: "[NUT] Multistream Title Updater | Update Twitch Title" + }, + args: { + title: title + } + + }); + + ws.send(request); +} + +function sbUpdateYouTubeTitle(ws, title, broadcastId) { + let request = JSON.stringify({ + request: "DoAction", + id: generateUUID(), + action: { + name: "[NUT] Multistream Title Updater | Update YouTube Title" + }, + args: { + broadcastId: broadcastId, + title: title + } + + }); + + ws.send(request); +} + + +////////////////////// +// HELPER FUNCTIONS // +////////////////////// + +function generateUUID() { // Public Domain/MIT + var d = new Date().getTime();//Timestamp + var d2 = ((typeof performance !== 'undefined') && performance.now && (performance.now() * 1000)) || 0;//Time in microseconds since page-load or 0 if unsupported + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { + var r = Math.random() * 16;//random number between 0 and 16 + if (d > 0) {//Use timestamp until depleted + r = (d + r) % 16 | 0; + d = Math.floor(d / 16); + } else {//Use microseconds since page-load if supported + r = (d2 + r) % 16 | 0; + d2 = Math.floor(d2 / 16); + } + return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16); + }); +} + +function IsNullOrWhitespace(str) { + return /^\s*$/.test(str); +} + +function SetElementVisibility(elementID, visibility) { + let element = document.getElementById(elementID); + if (visibility) + element.style.display = 'inline'; + else + element.style.display = 'none'; +} + +function ReadLinesFromFile(filePath) { + return new Promise((resolve, reject) => { + const request = new XMLHttpRequest(); + request.open('GET', filePath, true); + request.onload = function () { + if (request.status === 200) { + resolve(request.responseText.split(/\r?\n/)); + } else { + reject(new Error(`File loading failed with status: ${request.status}`)); + } + }; + request.onerror = function () { + reject(new Error('Network error occurred during file loading.')); + }; + request.send(); + }); +} + + + +/////////////////////////////////// +// STREAMER.BOT WEBSOCKET STATUS // +/////////////////////////////////// + +// This function sets the visibility of the Streamer.bot status label on the overlay +function SetConnectionStatus(connected) { + let connectionStatusIcon = document.getElementById("connectionStatusIcon"); + let ThisIsWhereAllTheCoolStuffHappens = document.getElementById("ThisIsWhereAllTheCoolStuffHappens"); + + if (connected) { + connectionStatusIcon.src = "connected.svg"; + ThisIsWhereAllTheCoolStuffHappens.classList.remove('disabled'); + SetElementVisibility("infoIcon", false); + SetElementVisibility("streamerbotConnectInstructions", false); + } + else { + connectionStatusIcon.src = "disconnected.svg"; + ThisIsWhereAllTheCoolStuffHappens.classList.add('disabled'); + SetElementVisibility("infoIcon", true); + SetElementVisibility("streamerbotConnectInstructions", true); + + // (1) Clear list of broadcasts + const fieldsContainer = document.getElementById('fieldsContainer'); + fieldsContainer.innerHTML = ""; + } +} + +// This function sets the visibility of the Streamer.bot status label on the overlay +function RefreshAnimation() { + let refreshContainer = document.getElementById("refreshContainer"); + refreshContainer.style.opacity = 1; + var tl = new TimelineMax(); + tl + .to(refreshContainer, 2, { opacity: 0, ease: Linear.easeNone }) +} + + + +// Button handling for UPDATE ALL button only +const infoIcon = document.querySelector("#infoIcon"); +infoIcon.addEventListener("click", function () { + window.open("https://www.notion.so/nutty-s-multistream-title-updater-e2fb7b24c7514f9cbd943729f54be1d9"); +}); + +// Button handling for UPDATE ALL button only +const submitButton = document.querySelector("#submitButton"); +submitButton.addEventListener("click", function () { + const titleInput = document.querySelector("#titleInput").value; + sbUpdateTitles(ws, titleInput); +}); + +// Button handling for REFRESH button only +const refreshButton = document.querySelector("#refreshButton"); +refreshButton.addEventListener("click", function () { + sbFetchBroadcasts(ws); +}); + + +function UpdateBroadcastList(data) { + // Iterate through broadcast list + // Find div whose ID matches the broadcast + // If it exists, update the title + // else, it's a new broadcast, so add it to the list + const fieldsContainer = document.getElementById('fieldsContainer'); + for (const broadcast of data.broadcastList) { + const broadcastDiv = fieldsContainer.querySelector(`#${broadcast.id}`); + + if (broadcastDiv == null) + AddBroadcast(broadcast); + else + UpdateBroadcast(broadcastDiv, broadcast); + } + + // Check if any broadcasts have gone offline + // If so, delete them from the list + var parentDiv = document.getElementById('fieldsContainer'); + const childDivs = parentDiv.querySelectorAll("div"); + childDivs.forEach(childDiv => { + var result = data.broadcastList.find(obj => { + return obj.id === childDiv.id + }) + if (result == null) + childDiv.innerHTML = ""; + }); + + //// THIS CODE ALSO WORKS AND IS WAY SIMPLER, I JUST MADE THINGS + //// 10 TIMES HARDER FOR MYSELF BECAUSE I LOVE PAIN POGGERS + // // (1) Clear list of broadcasts + // const fieldsContainer = document.getElementById('fieldsContainer'); + // fieldsContainer.innerHTML = ""; + + // // (2) For each broadcast in list, create new entry + // for (const broadcast of data.broadcastList) + // { + // AddBroadcast(broadcast); + // } + + // Count the number of YouTube broadcasts + // If this is 0, put a message to tell the user that they need to go live on YouTube first + // Else, hide that message + const youtubeBroadcastCount = data.broadcastList.reduce((count, broadcast) => count + (broadcast.platform === "youtube" ? 1 : 0), 0); + if (youtubeBroadcastCount <= 0) + SetElementVisibility("noYouTubeStreamsInstructions", true); + else + SetElementVisibility("noYouTubeStreamsInstructions", false); + + RefreshAnimation(); +} + +function AddBroadcast(broadcast) { + // Get a reference to the template + const template = document.getElementById('platformTemplate'); + + // Create a new instance of the template + const instance = template.content.cloneNode(true); + + // Assign ID to instance + const broadcastBox = instance.querySelector('.broadcastBox'); + broadcastBox.id = broadcast.id; + + // Modify the content of the template instance + const titleElement = instance.querySelector('.platformTitle'); + titleElement.value = broadcast.title; + + // Modify the content of the template instance + const buttonElement = instance.querySelector('.platformSubmitButton'); + buttonElement.classList.add(broadcast.platform); + + // Modify the icon of the template instance + const iconElement = instance.querySelector('#icon'); + + // Add button handling (need separate handling for Twitch/YouTube) + switch (broadcast.platform) { + case 'twitch': + iconElement.src = 'twitch.png'; + + buttonElement.addEventListener("click", function () { + sbUpdateTwitchTitle(ws, titleElement.value); + }); + break; + case 'youtube': + // Modify the icon of the template instance + iconElement.src = 'youtube.png'; + + buttonElement.addEventListener("click", function () { + const youtubeID = broadcast.id.replace('youtube-', ''); + sbUpdateYouTubeTitle(ws, titleElement.value, youtubeID); + }); + break; + } + + // Add click event + const iconButton = instance.querySelector('#platformIconButton'); + iconButton.addEventListener("click", function () { + window.open(broadcast.url); + }); + + // Insert the modified template instance into the DOM + const fieldsContainer = document.getElementById('fieldsContainer'); + fieldsContainer.appendChild(instance); +} + +function UpdateBroadcast(div, broadcast) { + // Modify the content of the template instance + const titleElement = div.querySelector('.platformTitle'); + titleElement.value = broadcast.title; +} + +connectws(); \ No newline at end of file diff --git a/multistream-title-updater/style.css b/.old/multistream-title-updater/style.css similarity index 100% rename from multistream-title-updater/style.css rename to .old/multistream-title-updater/style.css diff --git a/multistream-title-updater/twitch.png b/.old/multistream-title-updater/twitch.png similarity index 100% rename from multistream-title-updater/twitch.png rename to .old/multistream-title-updater/twitch.png diff --git a/multistream-title-updater/youtube.png b/.old/multistream-title-updater/youtube.png similarity index 100% rename from multistream-title-updater/youtube.png rename to .old/multistream-title-updater/youtube.png diff --git a/.templates/obs-dock/config.json b/.templates/obs-dock/config.json new file mode 100644 index 0000000..32ca26f --- /dev/null +++ b/.templates/obs-dock/config.json @@ -0,0 +1,6 @@ +{ + "title": "Widget", + "requiredSbActions": [ + ], + "sbImportCode": "U0JBRR+LCAAAAAAABADtW9ty4ki2fZ+I+QdHndeWIzN1QZqI82AwCGFMF8Ig0Lgf8iIJFZKgQYDxTP/72SkJbAzY1RNn3F0T4yiXjXZe9nXtlYn5x1//cnX1JQ1y+uVvV/+QL+BlRtMAXn75uoyzPFhe1ef5l58qGV3n0/lSSrN1nu8OzzfBchXPMynA1/gaHQQiWPFlvMgr4euF5u46u+GVJFsnyV6WxlmcrtPRYU0plLLfihFfBD3SlxZrrODJ38snV3tRIY6F3JgEGmKEEYXWdKxoOtIUhhlVqEFM1cBU03i4V66Y9us6WBduQNWXcua//dfRzCCjLAnkrvlyHRxJnniyFkFrOU/b8SqfL3cwKKTJ6tKor0Em4iw6N+pMlK7+edXcBFm+OlInWs7Xi0PErqPoSEqTLd2tIBLn9ljSTMzTQ4xO5Hye8fVyCVuek+bLOIoghq8D8yY4+3DDEIzQT28F9AkECn77vIyogVCIVI6UwDQpRLRGFUsz4LdAEIQ1YhDd+vJ2ar5bBMVm5K3kYtReYrLap9kvr6W//fSecXlcegAb561QWU0liNcUbBiWoolaTTE1RBSNqUYtJGrILHrZCvUPt6IM3mmM3o+dhrBhUWQqIhShotW4UKjQsMIZskyhUpWE+LLV2g9n9WrNHkrtT5N8nuXTlR2HeWEA1s97TKiasHTGwE8YPEYwUSxu6AqluopEGNLQesdjJ4v+yB4rHVIzQ0sNCVEIYYGiYYtB4YBDDI1olBhYM3XzskNO9Ps3OaQ0+8T971eHCiYYoeCAZ2FN0TSmKdQ0iRKoODTCUDNNi102rfZJppW6WoamQekKxSQahAGpumIJqGkjCJBaMzVdZeElXTWEzE+NgwLM5FIoTiWVgTqrYU23lJpRM8FAyRlQGCg6NQnYZ2BT5e8Y+FmJVqGEGYga1XVFpTVgOYJwKIoAKZQFIcKUcTUwLit7ij1/umioFlIDaglFmEgaqJqKyQRWiI4CZmosZGHtnWh8bm1oBtEwQLNS06DVaUFNUyzV0gG9BQ94aOqopl5SVtWBw3yqtiHBvGZh8CrWAXVMXdY04KuFQgOpDBDpfW0/K3mqshTc0IBEKKbOgDcRThVARXjJVG5CWwgBk97T9nPr0gprQhWEKkQjkquGSGEMWrigcPBQdZ0jA72n7Wfl7b9emEhnguAAeKwmGR0xDMh16AgGN7BQkUks9WJhYuDrf3oDMVE1avFACSBkQFk1Ca1QI4Guq9TQAvh3kYD9CPYx6FWGEJqCORxuoZFD5TOtBm0Pa2CysLB+sc+RT290FrewpatcqQVSWx4aikkp/IdVlVKOKOYXG53U9nOhFZs18CDBSojgfAe5g4FD8FDBNRLwgGmhjsl72n5W+e/vFP5+F/PZNZvnv1y5ND7F0UDeMvSqwTMY62R8DqkXnR1d+QAMhVQClm6oQBSNUFUoVg1FRcQMTWwKoesXAcJE/58H9pcXv7y+r4Ajx83pVdI5P21oUt4NNeYiOFGaz5Pyiux/TGSiVuuCQwhRCVfh7BKYhryRCgBQzECegQNMgYzRcx1sG8TRVN62oNM6PpwAkPVWtKDyksaR+766aPtOV8aZCCR2oN+fSCf6X7gOPMiXQRiAqjw4iUEhbvzt8dEDfebb1ePjfcyX89U8zK97zYfHx9YSNt3OlzNDe3zcaNfoWkUqth4f0xWfL5OYXYskebvhv7rmYLfKg7RY8XjBX95axHZ5UKSJpOfj3oKlPBqqybOwR/nPW3S3f/aQjlRhW2tOrFQ09Dv4uf5I3p09LVjWrN32571GVseT9Gkx2dW/Mbv1zHf122Fz2mHwjKVDkK96jSjZCq+zot59NEmtDWvUW4E9+ibGbnLXmEm5XKvTGC6+TtJFMlH7kW+30GRwYznNw7P10G7tOBnOB1lvw2bwnfkJz+A5Ge2o11rR8SJ5IJ1ffa+H7mCsr94vuom7GaouyPUM9lpI2xr9ZDYZu1Nv1wnHam8jxp1v/qAjnFsU9XHd6c5EIpoj4ns6GrWTrT+I3q5T6dvrDxr62Pc6z4z0lv7YbfDUmvp2b8pVt/DVne1Ohd1cj2zrQbQ70qfzu0E1v1/osoN5mKdaBHLsPxT+sm77i9NrxMUyAMBbxElw5uKzSvSE7gY5XZ67Gi1GrOgmcIPVOskf5iO6jGUJvjf2aNRp5ZSgQg1MsIUMRfJK6IuGqVCVBIpFBNJqwIwN9bTTfAeoWPLr34Iq+HugGSA1oYtVIGx5mX0M6S9IdHrrr/OabuhYthoBNIGY8naCcYUAuqqaZmEVH9/d/5i3/sWrK3e+zuPsqB/9OS7//8guW+OYchWHiq7rkAGhThQayGuR0ERcR8AUySlR/G+X/U/tsvtn3ZmeiFtA+Gz0LDuJ0+rp0CkSNtBvGXlayc5FPei2O70N3SfhafLt93Trl32Sr+efu4uJJ9ZMlfv0ns+OSTqJnz5Bx3UXjGjQqRaYk2Tt7+oPwbiHfA+th9loLewk9wd6h2Uu6GltZRcWoK8/vj87Z2SPtAu2D2m7k0w8V+rxe1jFc9VRgVW0nv1RHTptFH0d3MDBIK/zdr3JCJ5ST1v31dGOp6Bz090wr7Vgsd6YjHuJa0P3BZ8ztaOHno5F29W7qcjGract+B5sHO26XmcjGk7RsXnFABz7aeNjd+rvgKE0Omwo9ZB+iOsNprrC+Sb1g+4+PthBGElmTjNZ03F/fic7/1iOmXUKxiF9Tgr/70BX+GnlI1jTJwLWjOJy/X40SK0Y/JbA73G3cROzbIREOwlhXMj3OiRWDDrsX4cU5gDLqebU1/5YsPGgXnPSF3udeBs5SV3ahvyxE/VTCzkZKtnSGEk9v5/FDcyN03zaTLx+BDmBIE/WwMRe2erugqFkWMDQdlEMcfnme7B2czp8GG6ljsDI+tHB1zI3yGjB264zauL7u4fV5bXs1tYfd8bUcxFtOCvHtrBo1AvfHcuOGNyQZ0kbWFidAztzsr0fOj9PPJxI3zB1hJx2D8mcgJw+5MZD2oIaeGetNMlYau38EcRFdYcwL/OhTvjuoFvMiAW/Xxr33tpQO/YIyTlsXLep95ScrntuzLv6PkOdPovGiX7V8yPmeqg/P00Au/Qp84ZQf/W28JJZgXVIYOpBvkONM7z3X7Rn4Yf5End4Gf99/cayPoHxI9rGfNwu9IefeSptYNLvY7SmbTdnVW02xof4AN7g7bDp2pLRdxqd2gs+TC2o7UIOP2sveo+sblaf+bFT4g/kV3dnRn0Vcg/y27GbpY62tZuMO7pT6VrML31xC3kM64/KHH2Ts5DTxZ6Dltt7OPLhbNMFbCvnDqPJoBwHeDTl2SwqbLD9BbOHB51cqDkYn8vTidOuTznk4QiwRWLfBOb8DHuxXX06IXCykWNsfwO4tJ2M6xKfABs6M1njVQ4A/rUgP+pNN56+0nFb2tuSz7dvaucmd273MZwd9IL82DEvWZf+SlL4Rh31vrQJYlPs5yXp3SGXbtJOXAeM0zdgz1COOWBedr8ofXTudHTwW7+oQ6hv0WgWelQ43Hebk/mR/ntsnBU5unLKfNrHZuZDTsH4obTflTEd34N9s9d7PYC/Z47dw1CfGwY27Wv0qJaqZ31izfxheYK8a3TWgJEoGFmZj60DdowJ9JVW9bpl/cpIJ+QEepSsvba7E97wyMfgIwQn1XUlK3IohBwq+lJ7FDM7+ea0/Q3U2yFvy1hOD3X2BmtXrzDtdX5GQVmf0HOj9YNtZQNPT9kuknW5YkQMgUPAybzMnRKjpT9vomL+7TwK1OP8DwfOvuYLDOomYkdBv773tCp4QeM4997o+aoPneupRzl8sLXAhqasnRHkvjuHOB9kEs/BZ6iMdcEbJMbJXCrrJBELv+0e9em3eAA5tQEbJTeR9batchFRqGNuu2mRF6P6jpEeYPr9q/HAGWzAxiJ/LeBLvefqRqGM82Gc/iDs1k4AZ/BTc13oOtxj7M38DK5VOmAkMZvjJy5IDnWAV8Antr49gd4/hX5W4c/4oNt6qLqANy7wK38DsQEfuRsBeB1ArCSGDvc64WNZdzZd+LLHfCt99AaHS32axU2M1Dl2qnFVPULPEc9OYwUcCrjJAG8hr5LuuPS9086PYhF6OL8834kO871pQj0xB3zB97f1nA2cg72yr/fHnQxisCz0aaDcBzyaAK/xB/gZuOEO/PdMizzJZa7EVd4e2XL3jCInvsm7XsFzoBd1dkx1cmq31r79VOZfwbM7yQc6r9kOxxOvt/TVjsTr2QfjZe+DWh/mNG0R6JHQC6BOGh/YCD0OaiYp9CWtVdcrekoO+xb598Gev07G/pSTHtRsHXU9ySlaOvSA7f0H+0p/Aq9dd8fuhjduth/sk0NvzYCv58AvkeTAzu3NB/7HU1gD+DyGPu6j3zF+V+XIOzr1gWtv44BMEfOeSu5U8REnvmj3zInrHHASOKy7he9z81746wzOEaio76KeGWDtyG5l4aDqbXZRR7KfGVAPZ9ZE8d1xTxwID3qlDXXefsEeYZvRBM6KIoUzWqNeYAJg9gbGzPxo768Si7pJD0EcUIn5uvQHBn60Fm1Zw0c1/QGOwJmmOl/J54Ab4Hv3a2lDX66FeDZKXuJ1dv+BD+dC2Q9EC55DfYBs5pb19r1rjKCvPlB7tGLNkQb6YMm1YG468Z6e/YfVu7i7x/yH1AL+o8+Yyj/c9+6YJ7+Siyn0PRtwAc5OCTTv4fb+9ubiOUuk1sJvVLnQOu5v/uvedfFssn05R9lv8hj6bHWOO8t/pP4lDzjhqAd7qhwqagB+n/sggz6z52L7/vgyHnoJS0eq0zrkzHx/nns3l2ZFHy/PY7AGKAV1CRyZjPqyt4ejJ17Wbz8H3Niwdg/OCj3I2xb4pXXa+96cX45q/1ZixXv1jV/67O5DHDiOx/gj7LjJ93bwl7NzNAYOydTeN8jXZNxwLmJcB3AUxtZecgN4wQfju/L+xy5450Vseu2f/+LNfwjevDq79clTMpFxHN8Uz6g3ie7O3x0U8goXOgVWDPRmiR/D+StM2u9xMuYtL9/Xf9ivMDBD//v+O1/n7rH/oDe+TE03TFMNFEFCoWg1LBTGNUthRoiQUC0Vhad/XvCDv/FV/rIfX753dfSGC0xPU5qJ44fbgK3mfBbkg2C5efMWzYuwkcTFR0leC/M43Y+XT6qP47x89oeUn0f4Ejwt5ss8EPLNrOJvP67RdfXHUacf7imkSKHJYkph1F//8tv/AVT//+2WNAAA" +} \ No newline at end of file diff --git a/.templates/obs-dock/contents/index.html b/.templates/obs-dock/contents/index.html new file mode 100644 index 0000000..6b37cd3 --- /dev/null +++ b/.templates/obs-dock/contents/index.html @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.templates/obs-dock/contents/script.js b/.templates/obs-dock/contents/script.js new file mode 100644 index 0000000..9eb8555 --- /dev/null +++ b/.templates/obs-dock/contents/script.js @@ -0,0 +1,45 @@ +////////////////////// +// GLOBAL VARIABLES // +////////////////////// + +const sbAction = ''; + + + +///////////////////////// +// STREAMER.BOT EVENTS // +///////////////////////// + +window.parent.sbClient.on('General.Custom', (response) => { + console.debug(response.data); + CustomEvent(response.data); +}) + + + +///////////////// +// PRINTER BOT // +///////////////// + + + + +////////////////////// +// HELPER FUNCTIONS // +////////////////////// + + + + + +/////////////////////// +// PAGE INTERACTIONS // +/////////////////////// + + + + + +/////////////////// +// PAGE SETTINGS // +/////////////////// \ No newline at end of file diff --git a/.templates/obs-dock/contents/style.css b/.templates/obs-dock/contents/style.css new file mode 100644 index 0000000..9fa4a22 --- /dev/null +++ b/.templates/obs-dock/contents/style.css @@ -0,0 +1,3 @@ +body { + margin: 1em; +} \ No newline at end of file diff --git a/.templates/obs-dock/index.html b/.templates/obs-dock/index.html new file mode 100644 index 0000000..332b16c --- /dev/null +++ b/.templates/obs-dock/index.html @@ -0,0 +1,20 @@ + + + + + + nutty + + + + + + + + + \ No newline at end of file diff --git a/.templates/obs-dock/script.js b/.templates/obs-dock/script.js new file mode 100644 index 0000000..2668f87 --- /dev/null +++ b/.templates/obs-dock/script.js @@ -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'; +}); \ No newline at end of file diff --git a/multistream-title-updater/config.json b/multistream-title-updater/config.json new file mode 100644 index 0000000..a6578a3 --- /dev/null +++ b/multistream-title-updater/config.json @@ -0,0 +1,18 @@ +{ + "title": "Stream Info", + "requiredSbActions": [ + { + "id": "9486774d-d706-41d8-85b4-7daff5cd1b0d", + "name": "Multistream Title Updater | Fetch Broadcasts" + }, + { + "id": "1c58eff0-e98a-4fab-86f0-6f5cee1d3ab3", + "name": "Multistream Title Updater | Update Stream Info" + }, + { + "id": "14da1d44-6e29-4582-92c3-2c59388be57e", + "name": "Multistream Title Updater | Open Link" + } + ], + "sbImportCode": "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" +} \ No newline at end of file diff --git a/printer-bot/icons/platforms/kick.png b/multistream-title-updater/contents/icons/platforms/kick.png similarity index 100% rename from printer-bot/icons/platforms/kick.png rename to multistream-title-updater/contents/icons/platforms/kick.png diff --git a/printer-bot/icons/platforms/kofi.png b/multistream-title-updater/contents/icons/platforms/kofi.png similarity index 100% rename from printer-bot/icons/platforms/kofi.png rename to multistream-title-updater/contents/icons/platforms/kofi.png diff --git a/printer-bot/icons/platforms/patreon.png b/multistream-title-updater/contents/icons/platforms/patreon.png similarity index 100% rename from printer-bot/icons/platforms/patreon.png rename to multistream-title-updater/contents/icons/platforms/patreon.png diff --git a/printer-bot/icons/platforms/tiktok.png b/multistream-title-updater/contents/icons/platforms/tiktok.png similarity index 100% rename from printer-bot/icons/platforms/tiktok.png rename to multistream-title-updater/contents/icons/platforms/tiktok.png diff --git a/printer-bot/icons/platforms/tipeeeStream.png b/multistream-title-updater/contents/icons/platforms/tipeeeStream.png similarity index 100% rename from printer-bot/icons/platforms/tipeeeStream.png rename to multistream-title-updater/contents/icons/platforms/tipeeeStream.png diff --git a/printer-bot/icons/platforms/twitch.png b/multistream-title-updater/contents/icons/platforms/twitch.png similarity index 100% rename from printer-bot/icons/platforms/twitch.png rename to multistream-title-updater/contents/icons/platforms/twitch.png diff --git a/multistream-title-updater/contents/icons/platforms/youtube-shorts.png b/multistream-title-updater/contents/icons/platforms/youtube-shorts.png new file mode 100644 index 0000000..54d2745 Binary files /dev/null and b/multistream-title-updater/contents/icons/platforms/youtube-shorts.png differ diff --git a/printer-bot/icons/platforms/youtube.png b/multistream-title-updater/contents/icons/platforms/youtube.png similarity index 100% rename from printer-bot/icons/platforms/youtube.png rename to multistream-title-updater/contents/icons/platforms/youtube.png diff --git a/multistream-title-updater/contents/index.html b/multistream-title-updater/contents/index.html new file mode 100644 index 0000000..0bb65b6 --- /dev/null +++ b/multistream-title-updater/contents/index.html @@ -0,0 +1,228 @@ + + + + + + + + + + + + + +
+ +
+
+ + +
+ + +
+ +
+ +
+ +
+
+
+ + +
+ + +
+ + + + +
+
+ +
+ / +
+
+ +
+ +
+ + +
+ + +
+ + +
+ + +
+ + +
+ +
+
+ +
+ / +
+
+ +
+ +
+ + +
+ +
+
+ +
+ Tag too long • / Tags +
+
+ + +
+ + +
+ + +
+ + +
+ + +
+ +
+
+ +
+ / +
+
+ +
+ +
+ + +
+ + +
+ + +
+ + +
+ + +
+ +
+
+ +
+ / +
+
+ +
+ +
+
+ +
+ / +
+
+ +
+ +
+ + +
+ +
+
+ +
+ / +
+
+ +
+ +
+ + +
+ + +
+ + + + + + + + \ No newline at end of file diff --git a/multistream-title-updater/contents/script.js b/multistream-title-updater/contents/script.js new file mode 100644 index 0000000..340c415 --- /dev/null +++ b/multistream-title-updater/contents/script.js @@ -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); \ No newline at end of file diff --git a/multistream-title-updater/contents/style.css b/multistream-title-updater/contents/style.css new file mode 100644 index 0000000..3e28841 --- /dev/null +++ b/multistream-title-updater/contents/style.css @@ -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; +} \ No newline at end of file diff --git a/multistream-title-updater/index.html b/multistream-title-updater/index.html index 4c088ec..332b16c 100644 --- a/multistream-title-updater/index.html +++ b/multistream-title-updater/index.html @@ -1,81 +1,20 @@ - - - - - + + + + nutty + -
Refreshed
- -
- - -
- - -
- -
-
Update Titles
- - -
- -
- -
-
-

-
- - -
-
- -
- -
-
- - - -
- - - -
+ + - - \ No newline at end of file + \ No newline at end of file diff --git a/multistream-title-updater/script.js b/multistream-title-updater/script.js index 72ec198..2668f87 100644 --- a/multistream-title-updater/script.js +++ b/multistream-title-updater/script.js @@ -1,444 +1,16 @@ -//////////// -// FIELDS // -//////////// +// Construct URL +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 urlParams = new URLSearchParams(queryString); +const configJson = "?config=" + baseURL + "config.json"; -const sbServerPort = urlParams.get("port") || 8080; -const sbServerAddress = urlParams.get("server") || "127.0.0.1"; +// Implement widget dock core +window.dockWrapper = document.getElementById('dock-wrapper'); +dockWrapper.src = `../../.common/core/widget-dock-core${configJson}`; -///////////////// -// GLOBAL VARS // -///////////////// - -let ws; - -/////////////////////////////////// -// SRTEAMER.BOT WEBSOCKET SERVER // -/////////////////////////////////// - -// This is the main function that connects to the Streamer.bot websocke server -function connectws() { - if ("WebSocket" in window) { - ws = new WebSocket("ws://" + sbServerAddress + ":" + sbServerPort + "/"); - - // Reconnect - ws.onclose = function () { - SetConnectionStatus(false); - setTimeout(connectws, 5000); - }; - - // Connect - ws.onopen = async function () { - SetConnectionStatus(true); - - console.log("Subscribe to events"); - ws.send( - JSON.stringify({ - request: "Subscribe", - id: "subscribe-events-id", - // This is the list of Streamer.bot websocket events to subscribe to - // See full list of events here: - // https://docs.streamer.bot/api/servers/websocket/requests - events: { - twitch: [ - "StreamUpdate" - ], - youTube: [ - "BroadcastStarted", - "BroadcastEnded", - "BroadcastUpdated" - ], - general: [ - "Custom" - ] - } - }) - ); - - sbGetActions(ws); - - ws.onmessage = function (event) { - // Grab message and parse JSON - const msg = event.data; - const wsdata = JSON.parse(msg); - - // Check if the user installed all the required Streamer.bot actions - if (wsdata.id == "GetActions") { - - // Check if all the required SB action exist - ReadLinesFromFile('requiredActions.txt') - .then(requiredActions => { - if (sbCheckRequiredActions(wsdata.actions, requiredActions)) { - SetElementVisibility("missingActionsInstructions", false); - sbFetchBroadcasts(ws); - } - else - SetElementVisibility("missingActionsInstructions", true); - }) - } - - if (typeof wsdata.event == "undefined") { - return; - } - - // Print data to log for debugging purposes - if (sbDebugMode) { - console.log(wsdata.data); - console.log(wsdata.event.source); - console.log(wsdata.event.type); - } - - // Check for events to trigger - // See documentation for all events here: - // https://wiki.streamer.bot/en/Servers-Clients/WebSocket-Server/Events - switch (wsdata.event.source) { - // Twitch Events - case 'Twitch': - switch (wsdata.event.type) { - case ('StreamUpdate'): - sbFetchBroadcasts(ws); - break; - } - // Twitch Events - case 'YouTube': - switch (wsdata.event.type) { - case ('BroadcastStarted'): - case ('BroadcastEnded'): - case ('BroadcastUpdated'): - sbFetchBroadcasts(ws); - break; - } - // General Events - case 'General': - switch (wsdata.event.type) { - case ('Custom'): - switch (wsdata.data.action) { - case ('[NUT] Multistream Title Updater | Fetch Broadcasts'): - UpdateBroadcastList(wsdata.data); - break; - } - break; - } - break; - - } - }; - } - } -} - - - -///////////////////////// -// STREAMER.BOT WIDGET // -///////////////////////// - -function sbGetActions(ws) { - - let request = JSON.stringify({ - request: "GetActions", - id: "GetActions" - }); - - ws.send(request); -} - -// Check if all the entries in targetActionNames exist in actionList -function sbCheckRequiredActions(actionList, targetActionNames) { - let foundActions = 0; - for (targetActionName of targetActionNames) { - if (actionList.some(action => action.name === targetActionName)) - foundActions++; - } - - return foundActions == targetActionNames.length -} - -function sbFetchBroadcasts(ws) { - - let request = JSON.stringify({ - request: "DoAction", - id: generateUUID(), - action: { - name: "[NUT] Multistream Title Updater | Fetch Broadcasts" - } - }); - - ws.send(request); -} - -function sbUpdateTitles(ws, title) { - let request = JSON.stringify({ - request: "DoAction", - id: generateUUID(), - action: { - name: "[NUT] Multistream Title Updater | Update All Broadcasts" - }, - args: { - title: title - } - - }); - - ws.send(request); -} - -function sbUpdateTwitchTitle(ws, title) { - let request = JSON.stringify({ - request: "DoAction", - id: generateUUID(), - action: { - name: "[NUT] Multistream Title Updater | Update Twitch Title" - }, - args: { - title: title - } - - }); - - ws.send(request); -} - -function sbUpdateYouTubeTitle(ws, title, broadcastId) { - let request = JSON.stringify({ - request: "DoAction", - id: generateUUID(), - action: { - name: "[NUT] Multistream Title Updater | Update YouTube Title" - }, - args: { - broadcastId: broadcastId, - title: title - } - - }); - - ws.send(request); -} - - -////////////////////// -// HELPER FUNCTIONS // -////////////////////// - -function generateUUID() { // Public Domain/MIT - var d = new Date().getTime();//Timestamp - var d2 = ((typeof performance !== 'undefined') && performance.now && (performance.now() * 1000)) || 0;//Time in microseconds since page-load or 0 if unsupported - return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { - var r = Math.random() * 16;//random number between 0 and 16 - if (d > 0) {//Use timestamp until depleted - r = (d + r) % 16 | 0; - d = Math.floor(d / 16); - } else {//Use microseconds since page-load if supported - r = (d2 + r) % 16 | 0; - d2 = Math.floor(d2 / 16); - } - return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16); - }); -} - -function IsNullOrWhitespace(str) { - return /^\s*$/.test(str); -} - -function SetElementVisibility(elementID, visibility) { - let element = document.getElementById(elementID); - if (visibility) - element.style.display = 'inline'; - else - element.style.display = 'none'; -} - -function ReadLinesFromFile(filePath) { - return new Promise((resolve, reject) => { - const request = new XMLHttpRequest(); - request.open('GET', filePath, true); - request.onload = function () { - if (request.status === 200) { - resolve(request.responseText.split(/\r?\n/)); - } else { - reject(new Error(`File loading failed with status: ${request.status}`)); - } - }; - request.onerror = function () { - reject(new Error('Network error occurred during file loading.')); - }; - request.send(); - }); -} - - - -/////////////////////////////////// -// STREAMER.BOT WEBSOCKET STATUS // -/////////////////////////////////// - -// This function sets the visibility of the Streamer.bot status label on the overlay -function SetConnectionStatus(connected) { - let connectionStatusIcon = document.getElementById("connectionStatusIcon"); - let ThisIsWhereAllTheCoolStuffHappens = document.getElementById("ThisIsWhereAllTheCoolStuffHappens"); - - if (connected) { - connectionStatusIcon.src = "connected.svg"; - ThisIsWhereAllTheCoolStuffHappens.classList.remove('disabled'); - SetElementVisibility("infoIcon", false); - SetElementVisibility("streamerbotConnectInstructions", false); - } - else { - connectionStatusIcon.src = "disconnected.svg"; - ThisIsWhereAllTheCoolStuffHappens.classList.add('disabled'); - SetElementVisibility("infoIcon", true); - SetElementVisibility("streamerbotConnectInstructions", true); - - // (1) Clear list of broadcasts - const fieldsContainer = document.getElementById('fieldsContainer'); - fieldsContainer.innerHTML = ""; - } -} - -// This function sets the visibility of the Streamer.bot status label on the overlay -function RefreshAnimation() { - let refreshContainer = document.getElementById("refreshContainer"); - refreshContainer.style.opacity = 1; - var tl = new TimelineMax(); - tl - .to(refreshContainer, 2, { opacity: 0, ease: Linear.easeNone }) -} - - - -// Button handling for UPDATE ALL button only -const infoIcon = document.querySelector("#infoIcon"); -infoIcon.addEventListener("click", function () { - window.open("https://www.notion.so/nutty-s-multistream-title-updater-e2fb7b24c7514f9cbd943729f54be1d9"); -}); - -// Button handling for UPDATE ALL button only -const submitButton = document.querySelector("#submitButton"); -submitButton.addEventListener("click", function () { - const titleInput = document.querySelector("#titleInput").value; - sbUpdateTitles(ws, titleInput); -}); - -// Button handling for REFRESH button only -const refreshButton = document.querySelector("#refreshButton"); -refreshButton.addEventListener("click", function () { - sbFetchBroadcasts(ws); -}); - - -function UpdateBroadcastList(data) { - // Iterate through broadcast list - // Find div whose ID matches the broadcast - // If it exists, update the title - // else, it's a new broadcast, so add it to the list - const fieldsContainer = document.getElementById('fieldsContainer'); - for (const broadcast of data.broadcastList) { - const broadcastDiv = fieldsContainer.querySelector(`#${broadcast.id}`); - - if (broadcastDiv == null) - AddBroadcast(broadcast); - else - UpdateBroadcast(broadcastDiv, broadcast); - } - - // Check if any broadcasts have gone offline - // If so, delete them from the list - var parentDiv = document.getElementById('fieldsContainer'); - const childDivs = parentDiv.querySelectorAll("div"); - childDivs.forEach(childDiv => { - var result = data.broadcastList.find(obj => { - return obj.id === childDiv.id - }) - if (result == null) - childDiv.innerHTML = ""; - }); - - //// THIS CODE ALSO WORKS AND IS WAY SIMPLER, I JUST MADE THINGS - //// 10 TIMES HARDER FOR MYSELF BECAUSE I LOVE PAIN POGGERS - // // (1) Clear list of broadcasts - // const fieldsContainer = document.getElementById('fieldsContainer'); - // fieldsContainer.innerHTML = ""; - - // // (2) For each broadcast in list, create new entry - // for (const broadcast of data.broadcastList) - // { - // AddBroadcast(broadcast); - // } - - // Count the number of YouTube broadcasts - // If this is 0, put a message to tell the user that they need to go live on YouTube first - // Else, hide that message - const youtubeBroadcastCount = data.broadcastList.reduce((count, broadcast) => count + (broadcast.platform === "youtube" ? 1 : 0), 0); - if (youtubeBroadcastCount <= 0) - SetElementVisibility("noYouTubeStreamsInstructions", true); - else - SetElementVisibility("noYouTubeStreamsInstructions", false); - - RefreshAnimation(); -} - -function AddBroadcast(broadcast) { - // Get a reference to the template - const template = document.getElementById('platformTemplate'); - - // Create a new instance of the template - const instance = template.content.cloneNode(true); - - // Assign ID to instance - const broadcastBox = instance.querySelector('.broadcastBox'); - broadcastBox.id = broadcast.id; - - // Modify the content of the template instance - const titleElement = instance.querySelector('.platformTitle'); - titleElement.value = broadcast.title; - - // Modify the content of the template instance - const buttonElement = instance.querySelector('.platformSubmitButton'); - buttonElement.classList.add(broadcast.platform); - - // Modify the icon of the template instance - const iconElement = instance.querySelector('#icon'); - - // Add button handling (need separate handling for Twitch/YouTube) - switch (broadcast.platform) { - case 'twitch': - iconElement.src = 'twitch.png'; - - buttonElement.addEventListener("click", function () { - sbUpdateTwitchTitle(ws, titleElement.value); - }); - break; - case 'youtube': - // Modify the icon of the template instance - iconElement.src = 'youtube.png'; - - buttonElement.addEventListener("click", function () { - const youtubeID = broadcast.id.replace('youtube-', ''); - sbUpdateYouTubeTitle(ws, titleElement.value, youtubeID); - }); - break; - } - - // Add click event - const iconButton = instance.querySelector('#platformIconButton'); - iconButton.addEventListener("click", function () { - window.open(broadcast.url); - }); - - // Insert the modified template instance into the DOM - const fieldsContainer = document.getElementById('fieldsContainer'); - fieldsContainer.appendChild(instance); -} - -function UpdateBroadcast(div, broadcast) { - // Modify the content of the template instance - const titleElement = div.querySelector('.platformTitle'); - titleElement.value = broadcast.title; -} - -connectws(); \ No newline at end of file +dockWrapper.addEventListener('load', () => { + dockWrapper.contentWindow.content.src = baseURL + '/contents'; +}); \ No newline at end of file diff --git a/printer-bot/config.json b/printer-bot/config.json new file mode 100644 index 0000000..cc9219c --- /dev/null +++ b/printer-bot/config.json @@ -0,0 +1,14 @@ +{ + "title": "Printer Bot", + "requiredSbActions": [ + { + "id": "2e40b2b2-a751-4504-b1ba-a628361a44cf", + "name": "Printer Bot | Events" + }, + { + "id": "5c756513-a1d0-4285-9dbc-21ad34491310", + "name": "Printer Bot | Print Routine" + } + ], + "sbImportCode": "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" +} \ No newline at end of file diff --git a/printer-bot/contents/icons/platforms/.old/kick.png b/printer-bot/contents/icons/platforms/.old/kick.png new file mode 100644 index 0000000..9cc9afa Binary files /dev/null and b/printer-bot/contents/icons/platforms/.old/kick.png differ diff --git a/printer-bot/contents/icons/platforms/.old/twitch.png b/printer-bot/contents/icons/platforms/.old/twitch.png new file mode 100644 index 0000000..dcbd6c8 Binary files /dev/null and b/printer-bot/contents/icons/platforms/.old/twitch.png differ diff --git a/printer-bot/contents/icons/platforms/.old/youtube.png b/printer-bot/contents/icons/platforms/.old/youtube.png new file mode 100644 index 0000000..5abdb7b Binary files /dev/null and b/printer-bot/contents/icons/platforms/.old/youtube.png differ diff --git a/printer-bot/contents/icons/platforms/kick.png b/printer-bot/contents/icons/platforms/kick.png new file mode 100644 index 0000000..1019501 Binary files /dev/null and b/printer-bot/contents/icons/platforms/kick.png differ diff --git a/printer-bot/contents/icons/platforms/twitch.png b/printer-bot/contents/icons/platforms/twitch.png new file mode 100644 index 0000000..82056eb Binary files /dev/null and b/printer-bot/contents/icons/platforms/twitch.png differ diff --git a/printer-bot/contents/icons/platforms/youtube.png b/printer-bot/contents/icons/platforms/youtube.png new file mode 100644 index 0000000..5ad3296 Binary files /dev/null and b/printer-bot/contents/icons/platforms/youtube.png differ diff --git a/printer-bot/contents/index.html b/printer-bot/contents/index.html new file mode 100644 index 0000000..7dc75f8 --- /dev/null +++ b/printer-bot/contents/index.html @@ -0,0 +1,76 @@ + + + + + + + + + + +
+
+
+
Printer Name
+
As labelled in Windows > Settings > Printers & Scanners
+
+ +
+
+
+
Paper Width (mm)
+
Printer Bot was designed for 80mm thermal paper
+
+ +
+
+
+
Ignore Test Triggers
+
In enabled, 'Test' and 'Simulate' events will be ignored
+
+ +
+
+
+
Delete Temp Files
+
Turn this off for debugging
+
+ +
+ +
+ + + + + + + + \ No newline at end of file diff --git a/printer-bot/contents/script.js b/printer-bot/contents/script.js new file mode 100644 index 0000000..fc3672c --- /dev/null +++ b/printer-bot/contents/script.js @@ -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 = ``; + 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 = ``; + const bitsElements = `${bits}` + 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 = 'First time subscriber!'; + + 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 = `${data.cumulative} months${data.monthStreak > 1 ? ' (' + data.monthStreak + ' in a row!)' : ''}`; + if (data.messageStripped) + messageEl.innerHTML += `

${data.messageStripped}`; + + 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 += `✦・゚ A mysterious admirer ・゚✦
`; + else + messageEl.innerHTML += `${data.user}
`; + messageEl.innerHTML += `gifted a ${data.tier} sub to
${data.recipientUser}`; + + 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 += `
From a mystery person...`; + else + subtitleEl.innerHTML += `
${data.user}`; + + const messageEl = document.createElement('div'); + if (data.totalGifts > 1) { + messageEl.innerHTML = `They've gifted ${data.totalGifts} subs in total!

`; + } + + // 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}
`; + }); + + 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 = `${data.user}
is raiding with a party of
${data.viewers} viewers!`; + + 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 = `${data.gifterUser}
gifted a membership to
${data.user}!`; + + 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 = `${data.user}
sent a Super Chat!`; + if (data.message) + messageEl.innerHTML += `

${data.message}`; + + 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 = `${data.user}
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 = `${data.duration} months`; + else + messageEl.innerHTML = 'First time subscriber!'; + + 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 = `${data.user}
gifted a sub to
${data["recipient.userName"]}!`; + + 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 = `${data.user}
gifted a sub to
${data["recipient.userName"]}!`; + + 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}
`; + } + + 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 = `${data.tipMessage}`; + + 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 = `${data.donationMessage}`; + + 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 = `${data["fw.message"]}`; + + 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}
`; + }); + 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 += `
${txt.value}`; + } + + // Add a cute thank you message because you're uwu like that + const thankYouEl = document.createElement('div'); + thankYouEl.innerHTML += `
Thank you for your purchase!`; + + 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 ${FormatCurrency(data["fw.amount"], data["fw.currency"])} 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 = `${data.user}
is hosting with a party of
${data.viewers} viewers!`; + + 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 + + + ${bodyContent} + +`.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))); \ No newline at end of file diff --git a/printer-bot/contents/style.css b/printer-bot/contents/style.css new file mode 100644 index 0000000..937b872 --- /dev/null +++ b/printer-bot/contents/style.css @@ -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; +} \ No newline at end of file diff --git a/printer-bot/index.html b/printer-bot/index.html index 34f0478..332b16c 100644 --- a/printer-bot/index.html +++ b/printer-bot/index.html @@ -1,49 +1,20 @@ - - - - - - + + + + nutty + -
-
-
- - -
-
- - -
- - -
-
+ + - - - \ No newline at end of file + \ No newline at end of file diff --git a/printer-bot/script.js b/printer-bot/script.js index 81e303b..2668f87 100644 --- a/printer-bot/script.js +++ b/printer-bot/script.js @@ -1,804 +1,16 @@ -/////////////////// -// PAGE ELEMENTS // -/////////////////// +// Construct URL +const currentURL = window.location.href; +let baseURL = currentURL; -const headerEl = document.getElementById('header'); -const contentEl = document.getElementById('content'); -const footerEl = document.getElementById('footer'); +if (baseURL.endsWith("index.html")) + baseURL = baseURL.replace("index.html", ""); -const avatarEl = document.getElementById('avatar'); -const titleEl = document.getElementById('title'); -const subtitleEl = document.getElementById('subtitle'); -const dateEl = document.getElementById('date'); +const configJson = "?config=" + baseURL + "config.json"; +// Implement widget dock core +window.dockWrapper = document.getElementById('dock-wrapper'); +dockWrapper.src = `../../.common/core/widget-dock-core${configJson}`; - -////////////////////// -// 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 = ``; - 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 = ``; - const bitsElements = `${bits}` - 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 = 'First time subscriber!'; - - 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 = `${data.cumulative} months${data.monthStreak > 1 ? ' (' + data.monthStreak + ' in a row!)' : ''}`; - if (data.messageStripped) - messageEl.innerHTML += `

${data.messageStripped}`; - - 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 += `${data.recipientUser}
received a ${data.tier} sub from
`; - if (data.anonymous) - messageEl.innerHTML += `a mysterious admirer...`; - else - messageEl.innerHTML += `${data.user}!`; - - 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 += `
From a mystery person...`; - else - subtitleEl.innerHTML += `
${data.user}`; - - const messageEl = document.createElement('div'); - if (data.totalGifts > 1) { - messageEl.innerHTML = `They've gifted ${data.totalGifts} subs in total!

`; - } - - // 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}
`; - }); - - 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 = `${data.user}
is raiding with a party of
${data.viewers} viewers!`; - - 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 = `${data.user}
received a membership from
${data.gifterUser}!`; - - 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 = `${data.user}
sent a Super Chat!`; - if (data.message) - messageEl.innerHTML += `

${data.message}`; - - 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 = `${data.user}
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 = `${data.duration} months`; - else - messageEl.innerHTML = 'First time subscriber!'; - - 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 = `${data["recipient.userName"]}
received a sub from
${data.user}!`; - - 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 = `${data["recipient.userName"]}
received a sub from
${data.user}!`; - - 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}
`; - } - - 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 = `${data.tipMessage}`; - - 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 = `${data.donationMessage}`; - - 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 = `${data["fw.message"]}`; - - 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}
`; - }); - 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 += `
${txt.value}`; - } - - // Add a cute thank you message because you're uwu like that - const thankYouEl = document.createElement('div'); - thankYouEl.innerHTML += `
Thank you for your purchase!`; - - 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 ${FormatCurrency(data["fw.amount"], data["fw.currency"])} 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 = `${data.user}
is hosting with a party of
${data.viewers} viewers!`; - - 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 - - - ${bodyContent} - -`.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(); - } -} \ No newline at end of file +dockWrapper.addEventListener('load', () => { + dockWrapper.contentWindow.content.src = baseURL + '/contents'; +}); \ No newline at end of file diff --git a/printer-bot/style.css b/printer-bot/style.css deleted file mode 100644 index fa6aad9..0000000 --- a/printer-bot/style.css +++ /dev/null @@ -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; -} \ No newline at end of file