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

-
- - -
-
- -
- -
-
- - - -
- - - -
- - - - \ No newline at end of file diff --git a/.old/multistream-title-updater/script.js b/.old/multistream-title-updater/script.js deleted file mode 100644 index 72ec198..0000000 --- a/.old/multistream-title-updater/script.js +++ /dev/null @@ -1,444 +0,0 @@ -//////////// -// 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/.templates/obs-dock/config.json b/.templates/obs-dock/config.json deleted file mode 100644 index 32ca26f..0000000 --- a/.templates/obs-dock/config.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "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 deleted file mode 100644 index 6b37cd3..0000000 --- a/.templates/obs-dock/contents/index.html +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - - - - - - \ No newline at end of file diff --git a/.templates/obs-dock/contents/script.js b/.templates/obs-dock/contents/script.js deleted file mode 100644 index 9eb8555..0000000 --- a/.templates/obs-dock/contents/script.js +++ /dev/null @@ -1,45 +0,0 @@ -////////////////////// -// 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 deleted file mode 100644 index 9fa4a22..0000000 --- a/.templates/obs-dock/contents/style.css +++ /dev/null @@ -1,3 +0,0 @@ -body { - margin: 1em; -} \ No newline at end of file diff --git a/.templates/obs-dock/index.html b/.templates/obs-dock/index.html deleted file mode 100644 index 332b16c..0000000 --- a/.templates/obs-dock/index.html +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - nutty - - - - - - - - - \ No newline at end of file diff --git a/.templates/obs-dock/script.js b/.templates/obs-dock/script.js deleted file mode 100644 index 2668f87..0000000 --- a/.templates/obs-dock/script.js +++ /dev/null @@ -1,16 +0,0 @@ -// 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 deleted file mode 100644 index a6578a3..0000000 --- a/multistream-title-updater/config.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "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/.common/core/widget-dock-core/icons/connected.svg b/multistream-title-updater/connected.svg similarity index 100% rename from .common/core/widget-dock-core/icons/connected.svg rename to multistream-title-updater/connected.svg diff --git a/multistream-title-updater/contents/icons/platforms/youtube-shorts.png b/multistream-title-updater/contents/icons/platforms/youtube-shorts.png deleted file mode 100644 index 54d2745..0000000 Binary files a/multistream-title-updater/contents/icons/platforms/youtube-shorts.png and /dev/null differ diff --git a/multistream-title-updater/contents/index.html b/multistream-title-updater/contents/index.html deleted file mode 100644 index 0bb65b6..0000000 --- a/multistream-title-updater/contents/index.html +++ /dev/null @@ -1,228 +0,0 @@ - - - - - - - - - - - - - -
- -
-
- - -
- - -
- -
- -
- -
-
-
- - -
- - -
- - - - -
-
- -
- / -
-
- -
- -
- - -
- - -
- - -
- - -
- - -
- -
-
- -
- / -
-
- -
- -
- - -
- -
-
- -
- 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 deleted file mode 100644 index 340c415..0000000 --- a/multistream-title-updater/contents/script.js +++ /dev/null @@ -1,546 +0,0 @@ -////////////////////// -// GLOBAL VARIABLES // -////////////////////// - -const sbActionFetchBroadcasts = '9486774d-d706-41d8-85b4-7daff5cd1b0d'; -const sbActionUpdateStreamInfo = '1c58eff0-e98a-4fab-86f0-6f5cee1d3ab3'; -const sbActionOpenUrl = '14da1d44-6e29-4582-92c3-2c59388be57e'; - -let currentBroadcastId = ''; -let runningActionId = ''; - - -/////////////////// -// PAGE ELEMENTS // -/////////////////// - -const blurLayer = document.getElementById('blur-layer'); -const updateAllDialog = document.getElementById('update-all-dialog'); -const updateTwitchDialog = document.getElementById('update-twitch-dialog'); -const updateKickDialog = document.getElementById('update-kick-dialog'); -const updateYouTubeDialog = document.getElementById('update-youtube-dialog'); -const broadcastList = document.getElementById('broadcast-list'); -const youtubeWarning = document.getElementById('youtube-warning'); - - - -///////////////////////// -// STREAMER.BOT EVENTS // -///////////////////////// - -window.addEventListener("message", (event) => { - FetchBroadcasts(); -}); - -window.parent.sbClient.on('General.Custom', (response) => { - console.debug(response.data); - GeneralCustom(response.data); -}) - - - -/////////////////////////////// -// MULTISTREAM TITLE UPDATER // -/////////////////////////////// - -async function GeneralCustom(data) { - // Only run if response matches the ID of the corresponding FetchBroadcasts() call - if (runningActionId != data.runningActionId) - return; - - switch(data.actionId) { - case sbActionFetchBroadcasts: - { - // Iterate through list of broadcasts and add it to the list - for (const broadcast of data.broadcastList) - await AddBroadcast(broadcast); - - // Check if YouTube account is connected - // If yes, count how many YouTube broadcasts there were - // If 0, show warning - const broadcasterInfo = await window.parent.sbClient.getBroadcaster(); - if (broadcasterInfo.platforms.youtube) - { - // Only show the warning if there are 0 monitored broadcasts - const ytBroadcastCount = data.broadcastList.filter(b => b.platform === "youtube").length; - if (ytBroadcastCount <= 0) - youtubeWarning.style.display = 'flex'; - else - youtubeWarning.style.display = 'none'; - - // Set the URL to the livestreaming dashboard - const broadcastButton = youtubeWarning.querySelector('#broadcast-dashboard-button'); - broadcastButton.onclick = function() { - //window.open(data.streamUrl, '_blank'); - OpenURL(`https://studio.youtube.com/channel/${broadcasterInfo.platforms.youtube.broadcastUserId}/livestreaming`); - }; - } - else - youtubeWarning.style.display = 'none'; - - // Check if any broadcasts have gone offline - // If so, delete them from the list - const childDivs = broadcastList.querySelectorAll(":scope > div"); - childDivs.forEach(childDiv => { - var result = data.broadcastList.find(obj => { - return obj.id === childDiv.id - }) - if (result == null) - childDiv.remove(); - }); - } - break; - } -} - -async function FetchBroadcasts() { - // Fetch from Streamer.bot - const response = await window.parent.sbClient.doAction({ id: sbActionFetchBroadcasts}); - runningActionId = response.args.runningActionId; -} - -async function AddBroadcast(data) { - // Get a reference to the template - const template = document.getElementById('broadcast-template'); - - const existingDiv = broadcastList.querySelector(`#${data.id}`); - - // Create a new instance of the template - let instance; - - if (existingDiv) - instance = existingDiv; - else { - instance = template.content.firstElementChild.cloneNode(true); - instance.id = data.id; - broadcastList.appendChild(instance); - } - - // Get divs - const platformIconEl = instance.querySelector('#platform-icon'); - const titleEl = instance.querySelector('#broadcast-title'); - const categoryEl = instance.querySelector('#broadcast-category'); - const kickWarningEl = instance.querySelector('#kick-warning'); - const streamButtonEl = instance.querySelector('#broadcast-stream-button'); - const dashboardButtonEl = instance.querySelector('#broadcast-dashboard-button'); - const editButtonEl = instance.querySelector('#broadcast-edit-button'); - - // Streamer.bot does not provide title/category for Kick, so pull from API - if (data.platform == 'kick') - { - let response = await fetch('https://kick.com/api/v1/channels/' + data.userLogin); - let response_data = await response.json(); - - // The current title is only provided if the stream if currently live - if (response_data.livestream) - { - data.title = response_data.livestream.session_title; - data.category = response_data.livestream.categories[0].name; - kickWarningEl.style.display = 'none'; - } - else if (response_data.previous_livestreams.length > 0) - { - data.title = response_data.previous_livestreams[0].session_title; - data.category = response_data.previous_livestreams[0].categories[0].name; - kickWarningEl.style.display = 'inline'; - } - else - { - data.title = ''; - data.category = ''; - kickWarningEl.style.display = 'inline'; - } - } - - // Flash green to show that it updated - if (titleEl.textContent != data.title || categoryEl.textContent != data.category) - { - instance.style.backgroundColor = getComputedStyle(document.documentElement).getPropertyValue('--confirm-flash'); - setTimeout(() => { - instance.style.backgroundColor = ''; - }, 1000); - } - - // Set the platform icon - platformIconEl.src = `icons/platforms/${data.platform}.png`; - - // Special logo for YouTube shorts - if (data.platform == 'youtube') { - const targets = ["vertical", "shorts"]; - - const isShort = data.tags.some(item => - targets.some(target => item.toLowerCase() === target.toLowerCase()) - ); - if (isShort) - platformIconEl.src = `icons/platforms/youtube-shorts.png`; - } - - // Set the stream title - if (data.title) - titleEl.textContent = data.title; - - // Set the stream category - if (data.category) - categoryEl.textContent = data.category; - - streamButtonEl.onclick = function() { - //window.open(data.streamUrl, '_blank'); - OpenURL(data.streamUrl); - }; - - dashboardButtonEl.onclick = function() { - //window.open(data.dashboardUrl, '_blank'); - OpenURL(data.dashboardUrl); - }; - - editButtonEl.onclick = function() { - switch (data.platform) { - case 'twitch': - document.getElementById('twitch-title-input').value = data.title; - document.getElementById('twitch-category-input').value = data.category; - document.getElementById('twitch-tags-input').value = data.tags.join(", "); - ValidateTwitchDialog(); - updateTwitchDialog.style.display = "flex"; - break; - case 'kick': - document.getElementById('kick-title-input').value = titleEl.textContent; - document.getElementById('kick-category-input').value = categoryEl.textContent; - ValidateKickDialog(); - updateKickDialog.style.display = "flex"; - break; - case 'youtube': - currentBroadcastId = data.id; - document.getElementById('youtube-title-input').value = data.title; - document.getElementById('youtube-description-input').value = data.description; - document.getElementById('youtube-category-input').value = data.category; - document.getElementById('youtube-tags-input').value = data.tags.join(", "); - document.getElementById('youtube-privacy-select').value = data.privacy; - ValidateYouTubeDialog(); - updateYouTubeDialog.style.display = "flex"; - break; - } - blurLayer.style.display = "block"; - }; -} - - - - -////////////////////// -// HELPER FUNCTIONS // -////////////////////// - -async function OpenURL(url) { - await window.parent.sbClient.doAction( - action = { - id: sbActionOpenUrl - }, - args = { - url: url - } - ); -} - - - -/////////////////////// -// PAGE INTERACTIONS // -/////////////////////// - -function OpenUpdateAllDialog() { - updateAllDialog.style.display = "flex"; - blurLayer.style.display = "block"; -} - -function CloseUpdateAllDialog() { - updateAllDialog.style.display = "none"; - blurLayer.style.display = "none"; -} - -function CloseUpdateTwitchDialog() { - updateTwitchDialog.style.display = "none"; - blurLayer.style.display = "none"; -} - -function CloseUpdateKickDialog() { - updateKickDialog.style.display = "none"; - blurLayer.style.display = "none"; -} - -function CloseUpdateYouTubeDialog() { - updateYouTubeDialog.style.display = "none"; - blurLayer.style.display = "none"; -} - -async function UpdateAllSubmit() { - await window.parent.sbClient.doAction( - action = { - id: sbActionUpdateStreamInfo - }, - args = { - platform: 'all', - title: document.getElementById('all-title-input').value, - category: document.getElementById('all-category-input').value - } - ); - - CloseUpdateAllDialog(); -} - -async function UpdateTwitchSubmit() { - await window.parent.sbClient.doAction( - action = { - id: sbActionUpdateStreamInfo - }, - args = { - platform: 'twitch', - title: document.getElementById('twitch-title-input').value, - category: document.getElementById('twitch-category-input').value, - tags: document.getElementById('twitch-tags-input').value - } - ); - - CloseUpdateTwitchDialog(); -} - -async function UpdateKickSubmit() { - await window.parent.sbClient.doAction( - action = { - id: sbActionUpdateStreamInfo - }, - args = { - platform: 'kick', - title: document.getElementById('kick-title-input').value, - category: document.getElementById('kick-category-input').value - } - ); - - CloseUpdateKickDialog(); -} - -async function UpdateYouTubeSubmit() { - await window.parent.sbClient.doAction( - action = { - id: sbActionUpdateStreamInfo - }, - args = { - platform: 'youtube', - title: document.getElementById('youtube-title-input').value, - description: document.getElementById('youtube-description-input').value, - category: document.getElementById('youtube-category-input').value, - tags: document.getElementById('youtube-tags-input').value, - privacy: document.getElementById('youtube-privacy-select').value, - broadcastId: currentBroadcastId - } - ); - - CloseUpdateYouTubeDialog(); -} - - - -///////////////////// -// DATA VALIDATION // -///////////////////// - -const GLOBAL_TITLE_MAX = 100; -const TWITCH_TITLE_MAX = 140; -const TWITCH_TAGS_MAX = 10; -const KICK_TITLE_MAX = 100; -const YOUTUBE_TITLE_MAX = 100; -const YOUTUBE_DESCRIPTION_MAX = 5000; -const YOUTUBE_TAGS_MAX = 500; - -const allTitleInput = document.getElementById("all-title-input"); -const twitchTitleInput = document.getElementById("twitch-title-input"); -const twitchTagsInput = document.getElementById("twitch-tags-input"); -const kickTitleInput = document.getElementById("kick-title-input"); -const youtubeTitleInput = document.getElementById("youtube-title-input"); -const youtubeDescriptionInput = document.getElementById("youtube-description-input"); -const youtubeTagsInput = document.getElementById("youtube-tags-input"); - -// Function to update char count and handle validation -function ValidateUpdateAllDialog() { - // Get references to elements - const allTitleCharLimit = document.getElementById('all-title-char-limit'); - const allCharCount = document.getElementById('all-title-char-count'); - const allTitleCharMax = document.getElementById('all-title-char-max'); - const allUpdateButton = document.getElementById('all-submit-button'); - - // Validate title field - const currentLength = allTitleInput.value.length; - allCharCount.textContent = currentLength; - allTitleCharMax.textContent = GLOBAL_TITLE_MAX; - - if (currentLength > GLOBAL_TITLE_MAX) - allTitleCharLimit.style.color = getComputedStyle(document.documentElement).getPropertyValue('--error-text-color'); - else - allTitleCharLimit.style.color = ""; // reset to default - - // Set button interactability (yes, that is a word — look it up) - if (currentLength > GLOBAL_TITLE_MAX) - allUpdateButton.disabled = true; - else - allUpdateButton.disabled = false; -} - -// Function to update char count and handle validation -function ValidateTwitchDialog() { - // Get references to elements - const twitchTitleCharLimit = document.getElementById('twitch-title-char-limit'); - const twitchCharCount = document.getElementById('twitch-title-char-count'); - const twitchTitleCharMax = document.getElementById('twitch-title-char-max'); - const twitchTagsCharLimit = document.getElementById('twitch-tags-char-limit'); - const twitchTagsCharCount = document.getElementById('twitch-tags-char-count'); - const twitchTagsCharMax = document.getElementById('twitch-tags-char-max'); - const twitchTagsTooLongWarning = document.getElementById('twitch-tags-too-long-warning'); - const twitchUpdateButton = document.getElementById('twitch-submit-button'); - - // Validate title field - const currentLength = twitchTitleInput.value.length; - twitchCharCount.textContent = currentLength; - twitchTitleCharMax.textContent = TWITCH_TITLE_MAX; - - if (currentLength > TWITCH_TITLE_MAX) - twitchTitleCharLimit.style.color = getComputedStyle(document.documentElement).getPropertyValue('--error-text-color'); - else - twitchTitleCharLimit.style.color = ""; // reset to default - - // Validate tags field - // Split by comma, trim each tag, sum lengths - const tagsArray = twitchTagsInput.value - .split(',') - .map(tag => tag.trim()) - .filter(tag => tag.length > 0); - - const tagsLength = tagsArray.length; - twitchTagsCharCount.textContent = tagsLength; - twitchTagsCharMax.textContent = TWITCH_TAGS_MAX; - - if (tagsLength > TWITCH_TAGS_MAX) - twitchTagsCharLimit.style.color = getComputedStyle(document.documentElement).getPropertyValue('--error-text-color'); - else - twitchTagsCharLimit.style.color = ""; // reset to default - - // Also check that each tag is under 25 characters - const hasLongTag = tagsArray.some(tag => tag.length > 25); - if (hasLongTag) { - twitchTagsTooLongWarning.style.display = 'inline'; - twitchTagsCharLimit.style.color = getComputedStyle(document.documentElement).getPropertyValue('--error-text-color'); - } - else { - twitchTagsTooLongWarning.style.display = 'none'; - twitchTagsCharLimit.style.color = ""; // reset to default - } - - // Set button interactability (yes, that is a word — look it up) - if (currentLength > TWITCH_TITLE_MAX || tagsLength > TWITCH_TAGS_MAX || hasLongTag) - twitchUpdateButton.disabled = true; - else - twitchUpdateButton.disabled = false; -} - -// Function to update char count and handle validation -function ValidateKickDialog() { - // Get references to elements - const kickTitleCharLimit = document.getElementById('kick-title-char-limit'); - const kickCharCount = document.getElementById('kick-title-char-count'); - const kickTitleCharMax = document.getElementById('kick-title-char-max'); - const kickUpdateButton = document.getElementById('kick-submit-button'); - - // Validate title field - const currentLength = kickTitleInput.value.length; - kickCharCount.textContent = currentLength; - kickTitleCharMax.textContent = KICK_TITLE_MAX; - - if (currentLength > KICK_TITLE_MAX) - kickTitleCharLimit.style.color = getComputedStyle(document.documentElement).getPropertyValue('--error-text-color'); - else - kickTitleCharLimit.style.color = ""; // reset to default - - // Set button interactability (yes, that is a word — look it up) - if (currentLength > KICK_TITLE_MAX) - kickUpdateButton.disabled = true; - else - kickUpdateButton.disabled = false; -} - -function ValidateYouTubeDialog() { - // Get references to elements - const youtubeTitleCharLimit = document.getElementById('youtube-title-char-limit'); - const youtubeTitleCharCount = document.getElementById('youtube-title-char-count'); - const youtubeTitleCharMax = document.getElementById('youtube-title-char-max'); - const youtubeDescriptionCharLimit = document.getElementById('youtube-description-char-limit'); - const youtubeDescriptionCharCount = document.getElementById('youtube-description-char-count'); - const youtubeDescriptionCharMax = document.getElementById('youtube-description-char-max'); - const youtubeTagsCharLimit = document.getElementById('youtube-tags-char-limit'); - const youtubeTagsCharCount = document.getElementById('youtube-tags-char-count'); - const youtubeTagsCharMax = document.getElementById('youtube-tags-char-max'); - const youtubeUpdateButton = document.getElementById('youtube-submit-button'); - - // Validate title field - const titleLength = youtubeTitleInput.value.length; - youtubeTitleCharCount.textContent = titleLength; - youtubeTitleCharMax.textContent = YOUTUBE_TITLE_MAX; - - if (titleLength > YOUTUBE_TITLE_MAX) - youtubeTitleCharLimit.style.color = getComputedStyle(document.documentElement).getPropertyValue('--error-text-color'); - else - youtubeTitleCharLimit.style.color = ""; // reset to default - - // Validate description field - const descriptionLength = youtubeDescriptionInput.value.length; - youtubeDescriptionCharCount.textContent = descriptionLength; - youtubeDescriptionCharMax.textContent = YOUTUBE_DESCRIPTION_MAX; - - if (descriptionLength > YOUTUBE_DESCRIPTION_MAX) - youtubeDescriptionCharLimit.style.color = getComputedStyle(document.documentElement).getPropertyValue('--error-text-color'); - else - youtubeDescriptionCharLimit.style.color = ""; // reset to default - - // Validate tags field - // Split by comma, trim each tag, sum lengths - const tagsArray = youtubeTagsInput.value - .split(',') - .map(tag => tag.trim()) - .filter(tag => tag.length > 0); - - const tagsLength = tagsArray.reduce((sum, tag) => sum + tag.length, 0) + (tagsArray.length > 0 ? tagsArray.length - 1 : 0); - youtubeTagsCharCount.textContent = tagsLength; - youtubeTagsCharMax.textContent = YOUTUBE_TAGS_MAX; - - if (tagsLength > YOUTUBE_TAGS_MAX) - youtubeTagsCharLimit.style.color = getComputedStyle(document.documentElement).getPropertyValue('--error-text-color'); - else - youtubeTagsCharLimit.style.color = ""; // reset to default - - // Set button interactability (yes, that is a word — look it up) - if (titleLength > YOUTUBE_TITLE_MAX || descriptionLength > YOUTUBE_DESCRIPTION_MAX || tagsLength > YOUTUBE_TAGS_MAX) - youtubeUpdateButton.disabled = true; - else - youtubeUpdateButton.disabled = false; -} - -// Attach event listener -allTitleInput.addEventListener("input", ValidateUpdateAllDialog); -twitchTitleInput.addEventListener("input", ValidateTwitchDialog); -twitchTagsInput.addEventListener("input", ValidateTwitchDialog); -kickTitleInput.addEventListener("input", ValidateKickDialog); -youtubeTitleInput.addEventListener("input", ValidateYouTubeDialog); -youtubeDescriptionInput.addEventListener("input", ValidateYouTubeDialog); -youtubeTagsInput.addEventListener("input", ValidateYouTubeDialog); - -// Initial check (in case the input has prefilled text) -ValidateUpdateAllDialog(); -ValidateTwitchDialog(); -ValidateKickDialog(); -ValidateYouTubeDialog(); - - - - -//////////////////////////// -// REFRESH BROADCAST LIST // -//////////////////////////// - -setInterval(FetchBroadcasts, 5000); \ No newline at end of file diff --git a/multistream-title-updater/contents/style.css b/multistream-title-updater/contents/style.css deleted file mode 100644 index 3e28841..0000000 --- a/multistream-title-updater/contents/style.css +++ /dev/null @@ -1,85 +0,0 @@ -body { - display: flex; - flex-direction: column; - gap: 1em; - margin: 1em; -} - -#broadcast-list { - display: flex; - flex-direction: column; - gap: 0.5em; -} - -button { - gap: 0.5em; -} - -.broadcast { - background-color: var(--dialog-background); - border: 1px solid #40404080; - border-radius: 0.5em; - padding: 0.2em 0.5em; - - display: flex; - flex-direction: row; - align-items: center; - gap: 1em; - - transition: 0.4s; -} - -.broadcast-info { - display: flex; - flex-direction: column; -} - -#broadcast-buttons { - display: flex; - flex-direction: row; - align-items: center; - margin-left: auto; -} - -.platform-icon { - height: 2em; -} - -.button-icon { - height: 1em; -} - -.flip-that-shit-homie { - /* Flip that bad boy */ - transform: scaleX(-1); -} - -.blur { - display: none; -} - -.dialog { - display: none; - width: 90%; - max-height: 80%; - overflow-y: auto; -} - -.setting-attribute { - color: yellow; -} - -#kick-warning { - display: none; -} - -#youtube-warning { - display: none; -} - -.char-limit { - margin-left: auto; - text-align: right; - font-size: 0.8em; - opacity: 0.7; -} \ No newline at end of file diff --git a/.common/core/widget-dock-core/icons/disconnected.svg b/multistream-title-updater/disconnected.svg similarity index 100% rename from .common/core/widget-dock-core/icons/disconnected.svg rename to multistream-title-updater/disconnected.svg diff --git a/.old/multistream-title-updater/icons.afdesign b/multistream-title-updater/icons.afdesign similarity index 100% rename from .old/multistream-title-updater/icons.afdesign rename to multistream-title-updater/icons.afdesign diff --git a/multistream-title-updater/index.html b/multistream-title-updater/index.html index 332b16c..4c088ec 100644 --- a/multistream-title-updater/index.html +++ b/multistream-title-updater/index.html @@ -1,20 +1,81 @@ + - - - - nutty - + + + + - - +
Refreshed
+ +
+ + +
+ + +
+ +
+
Update Titles
+ + +
+ +
+ +
+
+

+
+ + +
+
+ +
+ +
+
+ + + +
+ + + +
- \ No newline at end of file + + \ No newline at end of file diff --git a/.old/multistream-title-updater/info.svg b/multistream-title-updater/info.svg similarity index 100% rename from .old/multistream-title-updater/info.svg rename to multistream-title-updater/info.svg diff --git a/.old/multistream-title-updater/requiredActions.txt b/multistream-title-updater/requiredActions.txt similarity index 98% rename from .old/multistream-title-updater/requiredActions.txt rename to multistream-title-updater/requiredActions.txt index c43224d..503a772 100644 --- a/.old/multistream-title-updater/requiredActions.txt +++ b/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/multistream-title-updater/script.js b/multistream-title-updater/script.js index 2668f87..72ec198 100644 --- a/multistream-title-updater/script.js +++ b/multistream-title-updater/script.js @@ -1,16 +1,444 @@ -// Construct URL -const currentURL = window.location.href; -let baseURL = currentURL; +//////////// +// FIELDS // +//////////// -if (baseURL.endsWith("index.html")) - baseURL = baseURL.replace("index.html", ""); +let sbDebugMode = true; -const configJson = "?config=" + baseURL + "config.json"; +const queryString = window.location.search; +const urlParams = new URLSearchParams(queryString); -// Implement widget dock core -window.dockWrapper = document.getElementById('dock-wrapper'); -dockWrapper.src = `../../.common/core/widget-dock-core${configJson}`; +const sbServerPort = urlParams.get("port") || 8080; +const sbServerAddress = urlParams.get("server") || "127.0.0.1"; -dockWrapper.addEventListener('load', () => { - dockWrapper.contentWindow.content.src = baseURL + '/contents'; -}); \ No newline at end of file +///////////////// +// 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/.old/multistream-title-updater/style.css b/multistream-title-updater/style.css similarity index 100% rename from .old/multistream-title-updater/style.css rename to multistream-title-updater/style.css diff --git a/.old/multistream-title-updater/twitch.png b/multistream-title-updater/twitch.png similarity index 100% rename from .old/multistream-title-updater/twitch.png rename to multistream-title-updater/twitch.png diff --git a/.old/multistream-title-updater/youtube.png b/multistream-title-updater/youtube.png similarity index 100% rename from .old/multistream-title-updater/youtube.png rename to multistream-title-updater/youtube.png diff --git a/printer-bot/config.json b/printer-bot/config.json deleted file mode 100644 index cc9219c..0000000 --- a/printer-bot/config.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "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 deleted file mode 100644 index 9cc9afa..0000000 Binary files a/printer-bot/contents/icons/platforms/.old/kick.png and /dev/null differ diff --git a/printer-bot/contents/icons/platforms/.old/twitch.png b/printer-bot/contents/icons/platforms/.old/twitch.png deleted file mode 100644 index dcbd6c8..0000000 Binary files a/printer-bot/contents/icons/platforms/.old/twitch.png and /dev/null differ diff --git a/printer-bot/contents/icons/platforms/.old/youtube.png b/printer-bot/contents/icons/platforms/.old/youtube.png deleted file mode 100644 index 5abdb7b..0000000 Binary files a/printer-bot/contents/icons/platforms/.old/youtube.png and /dev/null differ diff --git a/printer-bot/contents/icons/platforms/kick.png b/printer-bot/contents/icons/platforms/kick.png deleted file mode 100644 index 1019501..0000000 Binary files a/printer-bot/contents/icons/platforms/kick.png and /dev/null differ diff --git a/printer-bot/contents/icons/platforms/twitch.png b/printer-bot/contents/icons/platforms/twitch.png deleted file mode 100644 index 82056eb..0000000 Binary files a/printer-bot/contents/icons/platforms/twitch.png and /dev/null differ diff --git a/printer-bot/contents/icons/platforms/youtube.png b/printer-bot/contents/icons/platforms/youtube.png deleted file mode 100644 index 5ad3296..0000000 Binary files a/printer-bot/contents/icons/platforms/youtube.png and /dev/null differ diff --git a/printer-bot/contents/index.html b/printer-bot/contents/index.html deleted file mode 100644 index 7dc75f8..0000000 --- a/printer-bot/contents/index.html +++ /dev/null @@ -1,76 +0,0 @@ - - - - - - - - - - -
-
-
-
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 deleted file mode 100644 index fc3672c..0000000 --- a/printer-bot/contents/script.js +++ /dev/null @@ -1,790 +0,0 @@ -////////////////////// -// 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 deleted file mode 100644 index 937b872..0000000 --- a/printer-bot/contents/style.css +++ /dev/null @@ -1,78 +0,0 @@ -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/multistream-title-updater/contents/icons/platforms/kick.png b/printer-bot/icons/platforms/kick.png similarity index 100% rename from multistream-title-updater/contents/icons/platforms/kick.png rename to printer-bot/icons/platforms/kick.png diff --git a/multistream-title-updater/contents/icons/platforms/kofi.png b/printer-bot/icons/platforms/kofi.png similarity index 100% rename from multistream-title-updater/contents/icons/platforms/kofi.png rename to printer-bot/icons/platforms/kofi.png diff --git a/multistream-title-updater/contents/icons/platforms/patreon.png b/printer-bot/icons/platforms/patreon.png similarity index 100% rename from multistream-title-updater/contents/icons/platforms/patreon.png rename to printer-bot/icons/platforms/patreon.png diff --git a/multistream-title-updater/contents/icons/platforms/tiktok.png b/printer-bot/icons/platforms/tiktok.png similarity index 100% rename from multistream-title-updater/contents/icons/platforms/tiktok.png rename to printer-bot/icons/platforms/tiktok.png diff --git a/multistream-title-updater/contents/icons/platforms/tipeeeStream.png b/printer-bot/icons/platforms/tipeeeStream.png similarity index 100% rename from multistream-title-updater/contents/icons/platforms/tipeeeStream.png rename to printer-bot/icons/platforms/tipeeeStream.png diff --git a/multistream-title-updater/contents/icons/platforms/twitch.png b/printer-bot/icons/platforms/twitch.png similarity index 100% rename from multistream-title-updater/contents/icons/platforms/twitch.png rename to printer-bot/icons/platforms/twitch.png diff --git a/multistream-title-updater/contents/icons/platforms/youtube.png b/printer-bot/icons/platforms/youtube.png similarity index 100% rename from multistream-title-updater/contents/icons/platforms/youtube.png rename to printer-bot/icons/platforms/youtube.png diff --git a/printer-bot/index.html b/printer-bot/index.html index 332b16c..34f0478 100644 --- a/printer-bot/index.html +++ b/printer-bot/index.html @@ -1,20 +1,49 @@ + - - - - 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 2668f87..81e303b 100644 --- a/printer-bot/script.js +++ b/printer-bot/script.js @@ -1,16 +1,804 @@ -// Construct URL -const currentURL = window.location.href; -let baseURL = currentURL; +/////////////////// +// PAGE ELEMENTS // +/////////////////// -if (baseURL.endsWith("index.html")) - baseURL = baseURL.replace("index.html", ""); +const headerEl = document.getElementById('header'); +const contentEl = document.getElementById('content'); +const footerEl = document.getElementById('footer'); -const configJson = "?config=" + baseURL + "config.json"; +const avatarEl = document.getElementById('avatar'); +const titleEl = document.getElementById('title'); +const subtitleEl = document.getElementById('subtitle'); +const dateEl = document.getElementById('date'); -// Implement widget dock core -window.dockWrapper = document.getElementById('dock-wrapper'); -dockWrapper.src = `../../.common/core/widget-dock-core${configJson}`; -dockWrapper.addEventListener('load', () => { - dockWrapper.contentWindow.content.src = baseURL + '/contents'; -}); \ No newline at end of file + +////////////////////// +// 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 diff --git a/printer-bot/style.css b/printer-bot/style.css new file mode 100644 index 0000000..fa6aad9 --- /dev/null +++ b/printer-bot/style.css @@ -0,0 +1,127 @@ +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