diff --git a/.common/core/widget-dock-core/icons/connected.svg b/.common/core/widget-dock-core/icons/connected.svg new file mode 100644 index 0000000..4e0799a --- /dev/null +++ b/.common/core/widget-dock-core/icons/connected.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/.common/core/widget-dock-core/icons/disconnected.svg b/.common/core/widget-dock-core/icons/disconnected.svg new file mode 100644 index 0000000..962f8ef --- /dev/null +++ b/.common/core/widget-dock-core/icons/disconnected.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/.common/core/widget-dock-core/index.html b/.common/core/widget-dock-core/index.html new file mode 100644 index 0000000..2cecd25 --- /dev/null +++ b/.common/core/widget-dock-core/index.html @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + + + +
+
+ + + +
+ + +
+ + +
+ + +
+ +
+ +
+
+ + +
+
+ + +
+
+
+ + + +
+ +
+ + + + +
+ + + \ No newline at end of file diff --git a/.common/core/widget-dock-core/script.js b/.common/core/widget-dock-core/script.js new file mode 100644 index 0000000..f224816 --- /dev/null +++ b/.common/core/widget-dock-core/script.js @@ -0,0 +1,271 @@ +//////////////// +// 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'); + + + +///////////////////////// +// 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; + +window.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); + }, + + 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 ${window.sbClient.info.name} (${window.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() { + 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 OpenRequiredActionsDialog() { + sbRequiredActionsDialog.style.display = "flex"; + blurLayer.style.display = "block"; +} + +function CloseRequiredActionsDialog() { + sbRequiredActionsDialog.style.display = "none"; + blurLayer.style.display = "none"; +} \ No newline at end of file diff --git a/.common/core/widget-dock-core/style.css b/.common/core/widget-dock-core/style.css new file mode 100644 index 0000000..8eb352c --- /dev/null +++ b/.common/core/widget-dock-core/style.css @@ -0,0 +1,118 @@ +html, +body { + height: 100%; + display: flex; + flex-direction: column; +} + + + +/**************/ +/*** HEADER ***/ +/**************/ + +#header { + display: flex; + flex-direction: row; + align-items: center; + padding: 1em; + gap: 1em; + box-shadow: 0px 0px 10px rgba(0, 0, 0, 1); +} + +#header-end { + margin-left: auto; + align-items: center; + display: flex; + flex-direction: row; + gap: 1em; +} + +#header-end button { + padding: 0.2em; +} + + + +/*********************/ +/*** PAGE CONTENTS ***/ +/*********************/ + +#content { + flex: 1; + overflow: auto; + margin: 1em; +} + + + +/******************/ +/*** BLUR LAYER ***/ +/******************/ + +#blur-layer { + position: fixed; + inset: 0; + /* top:0; left:0; bottom:0; right:0 */ + backdrop-filter: blur(5px); + /* blur the content behind */ + -webkit-backdrop-filter: blur(5px); + /* Safari support */ + z-index: 1; + /* behind the modal */ +} + + + +/********************************/ +/*** STREAMER.BOT CONNECT BOX ***/ +/********************************/ + +#sb-required-actions-dialog { + max-width: 30em; + display: none; +} + +#sb-connect-button:enabled:hover { + background-color: #3be477; +} + +#sb-error-label { + background: #a82d2e33; + border-radius: 0.5em; + padding: 1em; + color: #c65e5e; + display: none; +} + + + +/******************************************/ +/*** STREAMER.BOT REQUIRED ACTIONS LIST ***/ +/******************************************/ + +.sb-required-action { + display: flex; + flex-direction: row; + align-items: center; +} + +.sb-required-action-found { + margin-left: auto; +} + +#sb-import-code { + white-space: nowrap; + overflow-x: auto; + overflow-y: hidden; + max-width: 100%; + padding: 0.5em; + font-family: monospace; + font-size: 1em; +} + +#sb-import-wrapper { + display: flex; + align-items: center; + gap: 0.5em; +} \ No newline at end of file diff --git a/.common/resources/logo.png b/.common/resources/logo.png new file mode 100644 index 0000000..0d8a815 Binary files /dev/null and b/.common/resources/logo.png differ diff --git a/.common/styles/global.css b/.common/styles/global.css new file mode 100644 index 0000000..4db99bf --- /dev/null +++ b/.common/styles/global.css @@ -0,0 +1,278 @@ +/*****************/ +/*** VARIABLES ***/ +/*****************/ + +:root { + --domain: nutty; + --background-color: #181818; + --accent-color: #2196f3; + --success-color: #3be477; + --button-color: #2e2e2e; + --dialog-background: #1d1d1d; + --callout-background: #181818; +} + + +@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; + color: white; + background-color: var(--background-color); +} + + + +/************/ +/*** TEXT ***/ +/************/ + +.title { + font-weight: 900; + font-size: 1.5em; + text-transform: uppercase; +} + +.field { + display: flex; + flex-direction: column; + gap: 0.5em; +} + +.setting-description { + font-size: 0.9em; + font-weight: 100; +} + + + +/***************/ +/*** 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; +} + +button:hover { + opacity: 1; + cursor: pointer; +} + +button:disabled { + opacity: 0.3; + cursor: inherit; +} + + + +/************************/ +/*** INPUT TEXT BOXES ***/ +/************************/ + +input { + border-radius: 0.5em; + width: auto; + padding: 0.5em; + background-color: #171717; + border: 1px solid #404040; + color: white; + font-size: 0.9em; +} + +input:disabled { + opacity: 0.5; +} + + + +/***********************/ +/*** 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); +} \ No newline at end of file diff --git a/printer-bot/config.json b/printer-bot/config.json new file mode 100644 index 0000000..cc9219c --- /dev/null +++ b/printer-bot/config.json @@ -0,0 +1,14 @@ +{ + "title": "Printer Bot", + "requiredSbActions": [ + { + "id": "2e40b2b2-a751-4504-b1ba-a628361a44cf", + "name": "Printer Bot | Events" + }, + { + "id": "5c756513-a1d0-4285-9dbc-21ad34491310", + "name": "Printer Bot | Print Routine" + } + ], + "sbImportCode": "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" +} \ No newline at end of file diff --git a/printer-bot/icons/platforms/kick.png b/printer-bot/contents/icons/platforms/kick.png similarity index 100% rename from printer-bot/icons/platforms/kick.png rename to printer-bot/contents/icons/platforms/kick.png diff --git a/printer-bot/icons/platforms/kofi.png b/printer-bot/contents/icons/platforms/kofi.png similarity index 100% rename from printer-bot/icons/platforms/kofi.png rename to printer-bot/contents/icons/platforms/kofi.png diff --git a/printer-bot/icons/platforms/patreon.png b/printer-bot/contents/icons/platforms/patreon.png similarity index 100% rename from printer-bot/icons/platforms/patreon.png rename to printer-bot/contents/icons/platforms/patreon.png diff --git a/printer-bot/icons/platforms/tiktok.png b/printer-bot/contents/icons/platforms/tiktok.png similarity index 100% rename from printer-bot/icons/platforms/tiktok.png rename to printer-bot/contents/icons/platforms/tiktok.png diff --git a/printer-bot/icons/platforms/tipeeeStream.png b/printer-bot/contents/icons/platforms/tipeeeStream.png similarity index 100% rename from printer-bot/icons/platforms/tipeeeStream.png rename to printer-bot/contents/icons/platforms/tipeeeStream.png diff --git a/printer-bot/icons/platforms/twitch.png b/printer-bot/contents/icons/platforms/twitch.png similarity index 100% rename from printer-bot/icons/platforms/twitch.png rename to printer-bot/contents/icons/platforms/twitch.png diff --git a/printer-bot/icons/platforms/youtube.png b/printer-bot/contents/icons/platforms/youtube.png similarity index 100% rename from printer-bot/icons/platforms/youtube.png rename to printer-bot/contents/icons/platforms/youtube.png diff --git a/printer-bot/contents/index.html b/printer-bot/contents/index.html new file mode 100644 index 0000000..bb744e3 --- /dev/null +++ b/printer-bot/contents/index.html @@ -0,0 +1,76 @@ + + + + + + + + + + +
+
+
+
Printer Name
+
As labelled in Windows > Settings > Printers & Scanners
+
+ +
+
+
+ +
Printer Bot was designed for 80mm thermal paper
+
+ +
+
+
+ +
In enabled, 'Test' and 'Simulate' events will be ignored
+
+ +
+
+
+ +
Turn this off for debugging
+
+ +
+ +
+ + + + + + + + \ No newline at end of file diff --git a/printer-bot/contents/script.js b/printer-bot/contents/script.js new file mode 100644 index 0000000..520a901 --- /dev/null +++ b/printer-bot/contents/script.js @@ -0,0 +1,790 @@ +////////////////////// +// GLOBAL VARIABLES // +////////////////////// + +const sbActionPrintRoutine = '5c756513-a1d0-4285-9dbc-21ad34491310'; +const avatarMap = new Map(); + + + +///////////////////////// +// STREAMER.BOT EVENTS // +///////////////////////// + +window.parent.sbClient.on('General.Custom', (response) => { + console.debug(response.data); + CustomEvent(response.data); +}) + + + +///////////////// +// PRINTER BOT // +///////////////// + +async function CustomEvent(data) { + if (data.actionName != 'Printer Bot | Events') + return; + + // Get a reference to the template + const template = document.getElementById('receipt-template'); + + // Create a new instance of the template + const instance = template.content.cloneNode(true); + + // Get divs + const headerEl = instance.querySelector('#receipt-header'); + const contentEl = instance.querySelector('#receipt-content'); + const footerEl = instance.querySelector('#receipt-footer'); + const avatarEl = instance.querySelector('#receipt-avatar'); + const titleEl = instance.querySelector('#receipt-title'); + const subtitleEl = instance.querySelector('#receipt-subtitle'); + const iconEl = instance.querySelector('#receipt-icon'); + const dateEl = instance.querySelector('#receipt-date'); + + // Set the main contents + switch (data.__source) { + // Twitch events + case ('TwitchCheer'): + { + avatarEl.src = await GetAvatar(data.userName, 'twitch'); + titleEl.innerText = `${data.bits} BITS`; + subtitleEl.innerText = `${data.user}`; + + const messageEl = document.createElement('div'); + messageEl.innerHTML = data.message; + + // Render emotes + for (i in data.emotes) { + const emoteElement = ``; + const emoteName = EscapeRegExp(data.emotes[i].name); + + let regexPattern = emoteName; + + // Check if the emote name consists only of word characters (alphanumeric and underscore) + if (/^\w+$/.test(emoteName)) { + regexPattern = `\\b${emoteName}\\b`; + } + else { + // For non-word emotes, ensure they are surrounded by non-word characters or boundaries + regexPattern = `(?<=^|[^\\w])${emoteName}(?=$|[^\\w])`; + } + + const regex = new RegExp(regexPattern, 'g'); + messageEl.innerHTML = messageEl.innerHTML.replace(regex, emoteElement); + } + + // Render cheermotes + for (i in data.cheerEmotes) { + const bits = data.cheerEmotes[i].bits; + const imageUrl = data.cheerEmotes[i].imageUrl; + const name = data.cheerEmotes[i].name; + const cheerEmoteElement = ``; + const bitsElements = `${bits}` + messageEl.innerHTML = messageEl.innerHTML.replace(new RegExp(`\\b${name}${bits}\\b`, 'i'), cheerEmoteElement + bitsElements); + } + + contentEl.appendChild(messageEl); + + // Set the platform icon + SetPlatformIcon(iconEl, 'twitch'); + } + break; + case ('TwitchSub'): + { + avatarEl.src = await GetAvatar(data.userName, 'twitch'); + titleEl.innerText = `${data.tier} subscriber`; + subtitleEl.innerText = `${data.user}`; + + const messageEl = document.createElement('div'); + messageEl.innerHTML = 'First time subscriber!'; + + contentEl.appendChild(messageEl); + + // Set the platform icon + SetPlatformIcon(iconEl, 'twitch'); + } + break; + case ('TwitchReSub'): + { + avatarEl.src = await GetAvatar(data.userName, 'twitch'); + titleEl.innerText = `${data.tier} subscriber`; + subtitleEl.innerText = `${data.user}`; + + const messageEl = document.createElement('div'); + messageEl.innerHTML = `${data.cumulative} months${data.monthStreak > 1 ? ' (' + data.monthStreak + ' in a row!)' : ''}`; + if (data.messageStripped) + messageEl.innerHTML += `

${data.messageStripped}`; + + contentEl.appendChild(messageEl); + + // Set the platform icon + SetPlatformIcon(iconEl, 'twitch'); + } + break; + case ('TwitchGiftSub'): + { + // Don't put profile pictures for subs that come from Gift Bombs to avoid rate limits + if (data.fromGiftBomb) + //avatarEl.style.display = 'none'; + return; + else + avatarEl.src = await GetAvatar(data.recipientUserName, 'twitch'); + titleEl.innerText = `Gifted Sub`; + + const messageEl = document.createElement('div'); + 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); + + window.parent.sbClient.doAction({ id: sbActionPrintRoutine }, { + receiptHTML: receiptHTML, + isTest: data.isTest, + printerName: document.getElementById('printer-name').value, + paperWidth: document.getElementById('paper-width').value, + ignoreTestTriggers: document.getElementById('ignore-test-triggers').checked, + deleteTempFiles: document.getElementById('delete-temp-files').checked + }); +} + + +////////////////////// +// HELPER FUNCTIONS // +////////////////////// + +async function GetAvatar(username, platform) { + + // First, check if the username is hashed already + if (avatarMap.has(`${username}-${platform}`)) { + console.debug(`Avatar found for ${username} (${platform}). Retrieving from hash map.`) + return avatarMap.get(`${username}-${platform}`); + } + + // If code reaches this point, the username hasn't been hashed, so retrieve avatar + switch (platform) { + case 'twitch': + { + console.debug(`No avatar found for ${username} (${platform}). Retrieving from Decapi.`) + let response = await fetch('https://decapi.me/twitch/avatar/' + username); + let data = await response.text(); + avatarMap.set(`${username}-${platform}`, data); + return data; + } + case 'kick': + { + console.debug(`No avatar found for ${username} (${platform}). Retrieving from Kick.`) + try { + let response = await fetch('https://kick.com/api/v2/channels/' + username); + console.log('https://kick.com/api/v2/channels/' + username) + let data = await response.json(); + let avatarURL = data.user.profile_pic; + if (!avatarURL) + avatarURL = 'https://kick.com/img/default-profile-pictures/default2.jpeg'; + avatarMap.set(`${username}-${platform}`, avatarURL); + return avatarURL; + } + catch (error) { + console.debug(error); + return 'https://kick.com/img/default-profile-pictures/default2.jpeg'; + } + } + } +} + +async function GetRenderedHTML(fragment) { + if (!(fragment instanceof DocumentFragment)) { + throw new Error('Argument must be a DocumentFragment'); + } + + // Filter out comment nodes from fragment content + const nodes = Array.from(fragment.childNodes).filter( + node => node.nodeType !== Node.COMMENT_NODE + ); + + const bodyContent = nodes + .map(node => node.outerHTML || node.textContent) + .join(''); + + // Get inline + + + ${bodyContent} + +`.trim(); + + return fullHTML; +} + +function ConvertWEBPToPNG(URL) { + return `https://images.weserv.nl/?url=${URL}&output=png`; +} + +function FormatCurrency(amount, currency) { + const isISOCode = /^[A-Z]{3}$/.test(currency); + + if (isISOCode) { + try { + return new Intl.NumberFormat(undefined, { + style: 'currency', + currency: currency, + currencyDisplay: 'symbol', + }).format(amount); + } catch { + return `${amount.toFixed(2)} ${currency}`; + } + } + + // Handle some common symbols that go before the number + const symbolsBefore = ['$', '€', '£', '¥', '₹']; + + if (symbolsBefore.includes(currency)) { + return `${currency}${amount.toFixed(2)}`; + } + + // Otherwise default to appending after + return `${amount.toFixed(2)} ${currency}`; +} + +function IsValidUrl(string) { + try { + new URL(string); + return true; + } catch { + return false; + } +} + +function EscapeRegExp(string) { + return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string +} + +function SetPlatformIcon(el, platform) { + // Set the platform icon + let baseURL = window.location.href; + baseURL = baseURL.replace(/index\.html$/i, ''); + + el.src = `${baseURL}/icons/platforms/${platform}.png`; +} + + + +/////////////////////// +// PAGE INTERACTIONS // +/////////////////////// + +let data = { + "__source": "TwitchSub", + "tier": "prime", + "isPrimeSub": true, + "monthsSubscribed": 1, + "isTest": false, + "actionName": "Printer Bot | Events", + "user": "nutty", + "userName": "nutty", + "userType": "twitch" +} + +async function TestPrint() { + CustomEvent(data); +} + + + +/////////////////// +// PAGE SETTINGS // +/////////////////// + +// Get references +const printerNameInput = document.getElementById('printer-name'); +const paperWidthInput = document.getElementById('paper-width'); +const ignoreTestTriggersInput = document.getElementById('ignore-test-triggers'); +const deleteTempFilesInput = document.getElementById('delete-temp-files'); + +// Local storage key must be prefixed with the first URL segment +const currentPath = window.location.pathname; +const urlSegment = currentPath.split('/').filter(Boolean)[0]; +const storageKey = (id) => `${urlSegment}::${id}`; + +function saveSetting(id) { + const el = document.getElementById(id); + const value = el.type === "checkbox" ? el.checked : el.value; + localStorage.setItem(storageKey(id), value); +} + +// Add event listeners +[printerNameInput, paperWidthInput, ignoreTestTriggersInput, deleteTempFilesInput].forEach(input => { + input.addEventListener("input", () => saveSetting(input.id)); + input.addEventListener("change", () => saveSetting(input.id)); +}); + +// Load settings +if (localStorage.getItem(storageKey(printerNameInput.id))) + printerNameInput.value = localStorage.getItem(storageKey(printerNameInput.id)); +if (localStorage.getItem(storageKey(paperWidthInput.id))) + paperWidthInput.value = localStorage.getItem(storageKey(paperWidthInput.id)); +if (localStorage.getItem(storageKey(ignoreTestTriggersInput.id))) + ignoreTestTriggersInput.checked = JSON.parse(localStorage.getItem(storageKey(ignoreTestTriggersInput.id))); +if (localStorage.getItem(storageKey(deleteTempFilesInput.id))) + deleteTempFilesInput.checked = JSON.parse(localStorage.getItem(storageKey(deleteTempFilesInput.id))); \ No newline at end of file diff --git a/printer-bot/contents/style.css b/printer-bot/contents/style.css new file mode 100644 index 0000000..27016eb --- /dev/null +++ b/printer-bot/contents/style.css @@ -0,0 +1,68 @@ +#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 { + text-align: center; + color: black; +} + +#receipt-header {} + +#receipt-content { + padding: 0.5em 0em; +} + +#receipt-footer {} + +#receipt-avatar { + width: 6em; + height: 6em; + 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: 1em; +} + +#receipt-date { + margin: 0.5em 0em; + font-size: 0.7em; + text-transform: uppercase; +} + +.emote { + height: 1em; +} \ No newline at end of file diff --git a/printer-bot/index.html b/printer-bot/index.html index 34f0478..332b16c 100644 --- a/printer-bot/index.html +++ b/printer-bot/index.html @@ -1,49 +1,20 @@ - - - - - - + + + + nutty + -
-
-
- - -
-
- - -
- - -
-
+ + - - - \ No newline at end of file + \ No newline at end of file diff --git a/printer-bot/script.js b/printer-bot/script.js index 81e303b..2807c6d 100644 --- a/printer-bot/script.js +++ b/printer-bot/script.js @@ -1,804 +1,16 @@ -/////////////////// -// PAGE ELEMENTS // -/////////////////// +// Construct URL +const currentURL = window.location.href; +let baseURL = currentURL; -const headerEl = document.getElementById('header'); -const contentEl = document.getElementById('content'); -const footerEl = document.getElementById('footer'); +if (baseURL.endsWith("index.html")) + baseURL = baseURL.replace("index.html", ""); -const avatarEl = document.getElementById('avatar'); -const titleEl = document.getElementById('title'); -const subtitleEl = document.getElementById('subtitle'); -const dateEl = document.getElementById('date'); +const configJson = "?config=" + baseURL + "config.json"; +// Implement widget dock core +window.dockWrapper = document.getElementById('dock-wrapper'); +dockWrapper.src = `../../.common/core/widget-dock-core${configJson}`; - -////////////////////// -// GLOBAL VARIABLES // -////////////////////// - -const avatarMap = new Map(); - - - -///////////////////////// -// STREAMER.BOT CLIENT // -///////////////////////// - -// Check local storage -if (localStorage.getItem('sbServerAddress') === null) - localStorage.setItem('sbServerAddress', '127.0.0.1'); -if (localStorage.getItem('sbServerPort') === null) - localStorage.setItem('sbServerPort', '8080'); - -document.getElementById('ip').value = localStorage.getItem('sbServerAddress'); -document.getElementById('port').value = localStorage.getItem('sbServerPort'); - -let sbServerAddress = document.getElementById('ip').value; -let sbServerPort = document.getElementById('port').value; - -let client = new StreamerbotClient({ - host: sbServerAddress, - port: sbServerPort, - - onConnect: (data) => { - console.log(`Streamer.bot successfully connected to ${sbServerAddress}:${sbServerPort}`) - console.debug(data); - - SetConnectionState(true); - }, - - onDisconnect: () => { - console.error(`Streamer.bot disconnected from ${sbServerAddress}:${sbServerPort}`) - SetConnectionState(false); - } -}); - -client.on('General.Custom', (response) => { - console.debug(response.data); - CustomEvent(response.data); -}) - - -//////////////////// -// STREAM PRINTER // -//////////////////// - -async function CustomEvent(data) { - if (data.actionName != 'Printer Bot | Events') - return; - - // Get a reference to the template - const template = document.getElementById('receipt-template'); - - // Create a new instance of the template - const instance = template.content.cloneNode(true); - - // Get divs - const headerEl = instance.querySelector('#header'); - const contentEl = instance.querySelector('#content'); - const footerEl = instance.querySelector('#footer'); - const avatarEl = instance.querySelector('#avatar'); - const titleEl = instance.querySelector('#title'); - const subtitleEl = instance.querySelector('#subtitle'); - const iconEl = instance.querySelector('#icon'); - const dateEl = instance.querySelector('#date'); - - // Set the main contents - switch (data.__source) { - // Twitch events - case ('TwitchCheer'): - { - avatarEl.src = await GetAvatar(data.userName, 'twitch'); - titleEl.innerText = `${data.bits} BITS`; - subtitleEl.innerText = `${data.user}`; - - const messageEl = document.createElement('div'); - messageEl.innerHTML = data.message; - - // Render emotes - for (i in data.emotes) { - const emoteElement = ``; - const emoteName = EscapeRegExp(data.emotes[i].name); - - let regexPattern = emoteName; - - // Check if the emote name consists only of word characters (alphanumeric and underscore) - if (/^\w+$/.test(emoteName)) { - regexPattern = `\\b${emoteName}\\b`; - } - else { - // For non-word emotes, ensure they are surrounded by non-word characters or boundaries - regexPattern = `(?<=^|[^\\w])${emoteName}(?=$|[^\\w])`; - } - - const regex = new RegExp(regexPattern, 'g'); - messageEl.innerHTML = messageEl.innerHTML.replace(regex, emoteElement); - } - - // Render cheermotes - for (i in data.cheerEmotes) { - const bits = data.cheerEmotes[i].bits; - const imageUrl = data.cheerEmotes[i].imageUrl; - const name = data.cheerEmotes[i].name; - const cheerEmoteElement = ``; - const bitsElements = `${bits}` - messageEl.innerHTML = messageEl.innerHTML.replace(new RegExp(`\\b${name}${bits}\\b`, 'i'), cheerEmoteElement + bitsElements); - } - - contentEl.appendChild(messageEl); - - // Set the platform icon - SetPlatformIcon(iconEl, 'twitch'); - } - break; - case ('TwitchSub'): - { - avatarEl.src = await GetAvatar(data.userName, 'twitch'); - titleEl.innerText = `${data.tier} subscriber`; - subtitleEl.innerText = `${data.user}`; - - const messageEl = document.createElement('div'); - messageEl.innerHTML = 'First time subscriber!'; - - contentEl.appendChild(messageEl); - - // Set the platform icon - SetPlatformIcon(iconEl, 'twitch'); - } - break; - case ('TwitchReSub'): - { - avatarEl.src = await GetAvatar(data.userName, 'twitch'); - titleEl.innerText = `${data.tier} subscriber`; - subtitleEl.innerText = `${data.user}`; - - const messageEl = document.createElement('div'); - messageEl.innerHTML = `${data.cumulative} months${data.monthStreak > 1 ? ' (' + data.monthStreak + ' in a row!)' : ''}`; - if (data.messageStripped) - messageEl.innerHTML += `

${data.messageStripped}`; - - contentEl.appendChild(messageEl); - - // Set the platform icon - SetPlatformIcon(iconEl, 'twitch'); - } - break; - case ('TwitchGiftSub'): - { - // Don't put profile pictures for subs that come from Gift Bombs to avoid rate limits - if (data.fromGiftBomb) - //avatarEl.style.display = 'none'; - return; - else - avatarEl.src = await GetAvatar(data.recipientUserName, 'twitch'); - titleEl.innerText = `Gifted Sub`; - - const messageEl = document.createElement('div'); - messageEl.innerHTML += `${data.recipientUser}
received a ${data.tier} sub from
`; - if (data.anonymous) - messageEl.innerHTML += `a mysterious admirer...`; - else - messageEl.innerHTML += `${data.user}!`; - - contentEl.appendChild(messageEl); - - // Set the platform icon - SetPlatformIcon(iconEl, 'twitch'); - } - break; - case ('TwitchGiftBomb'): - { - avatarEl.src = await GetAvatar(data.userName, 'twitch'); - titleEl.innerHTML = `${data.gifts} × Gifted Subs`; - subtitleEl.innerHTML += `✧*・゚✧ ${data.tier.toUpperCase()} ✧・゚*✧`; - if (data.anonymous) - subtitleEl.innerHTML += `
From a mystery person...`; - else - subtitleEl.innerHTML += `
${data.user}`; - - const messageEl = document.createElement('div'); - if (data.totalGifts > 1) { - messageEl.innerHTML = `They've gifted ${data.totalGifts} subs in total!

`; - } - - // Get a list of all recipient users - Object.keys(data) - .filter(key => /^gift\.recipientUser\d+$/.test(key)) - .forEach((key, index) => { - const username = data[key]; - messageEl.innerHTML += `${username}
`; - }); - - contentEl.appendChild(messageEl); - - // Set the platform icon - SetPlatformIcon(iconEl, 'twitch'); - } - break; - case ('TwitchRaid'): - { - avatarEl.src = await GetAvatar(data.userName, 'twitch'); - - const messageEl = document.createElement('div'); - messageEl.innerHTML = `${data.user}
is raiding with a party of
${data.viewers} viewers!`; - - contentEl.appendChild(messageEl); - - // Set the platform icon - SetPlatformIcon(iconEl, 'twitch'); - } - break; - - // YouTube Events - case ('YouTubeNewSponsor'): - { - if (data.userProfileUrl) - avatarEl.src = data.userProfileUrl; - else - avatarEl.style.display = 'none'; - - titleEl.innerText = `${data.levelName}`; - subtitleEl.innerText = `${data.user}`; - - contentEl.style.display = 'none'; - - // Set the platform icon - SetPlatformIcon(iconEl, 'youtube'); - } - break; - case ('YouTubeGiftMembershipReceived'): - { - if (data.gifterProfileUrl) - avatarEl.src = data.gifterProfileUrl; - else - avatarEl.style.display = 'none'; - titleEl.innerText = `Gifted Membership`; - - const messageEl = document.createElement('div'); - messageEl.innerHTML = `${data.user}
received a membership from
${data.gifterUser}!`; - - contentEl.appendChild(messageEl); - - // Set the platform icon - SetPlatformIcon(iconEl, 'youtube'); - } - break; - case ('YouTubeSuperChat'): - { - if (data.userProfileUrl) - avatarEl.src = data.userProfileUrl; - else - avatarEl.style.display = 'none'; - titleEl.style.fontSize = '2em'; - titleEl.innerText = `${data.amount}`; - - const messageEl = document.createElement('div'); - messageEl.innerHTML = `${data.user}
sent a Super Chat!`; - if (data.message) - messageEl.innerHTML += `

${data.message}`; - - contentEl.appendChild(messageEl); - - // Set the platform icon - SetPlatformIcon(iconEl, 'youtube'); - } - break; - case ('YouTubeSuperSticker'): - { - if (data.stickerImageUrl) - avatarEl.src = data.stickerImageUrl; - else - avatarEl.style.display = 'none'; - titleEl.style.fontSize = '2em'; - titleEl.innerText = `${data.amount}`; - - const messageEl = document.createElement('div'); - messageEl.innerHTML = `${data.user}
sent a Super Sticker!`; - - contentEl.appendChild(messageEl); - - // Set the platform icon - SetPlatformIcon(iconEl, 'youtube'); - } - break; - break; - - // Kick Events - case ('KickSubscription'): - case ('KickResubscription'): - { - avatarEl.src = ConvertWEBPToPNG(await GetAvatar(data.user, 'kick')); - titleEl.innerText = `Subscriber`; - subtitleEl.innerText = `${data.user}`; - - const messageEl = document.createElement('div'); - if (data.duration > 1) - messageEl.innerHTML = `${data.duration} months`; - else - messageEl.innerHTML = 'First time subscriber!'; - - contentEl.appendChild(messageEl); - - // Set the platform icon - SetPlatformIcon(iconEl, 'kick'); - } - break; - case ('KickGiftSubscription'): - { - avatarEl.src = ConvertWEBPToPNG(await GetAvatar(data["recipient.userLogin"], 'kick')); - titleEl.innerText = `Gifted Sub`; - - const messageEl = document.createElement('div'); - messageEl.innerHTML = `${data["recipient.userName"]}
received a sub from
${data.user}!`; - - contentEl.appendChild(messageEl); - - // Set the platform icon - SetPlatformIcon(iconEl, 'kick'); - } - break; - case ('KickMassGiftSubscription'): - { - // There is only one sub, so use the same template for a single gifted sub - if ('recipient.userName' in data) { - avatarEl.src = ConvertWEBPToPNG(await GetAvatar(data["recipient.userLogin"], 'kick')); - titleEl.innerText = `Gifted Sub`; - - const messageEl = document.createElement('div'); - messageEl.innerHTML = `${data["recipient.userName"]}
received a sub from
${data.user}!`; - - contentEl.appendChild(messageEl); - } - else { - avatarEl.src = ConvertWEBPToPNG(await GetAvatar(data.user, 'kick')); - - // Calculate how many subs were gived - let maxIndex = -1; - for (const key in data) { - const match = key.match(/^recipient\.(\d+)\./); - if (match) { - const index = parseInt(match[1], 10); - if (index > maxIndex) { - maxIndex = index; - } - } - } - const totalGifts = maxIndex + 1; - - titleEl.innerHTML = `${totalGifts} × Gifted Subs`; - subtitleEl.innerText = `${data.user}`; - - const messageEl = document.createElement('div'); - - // Loop through each recipient and include it in the receipt - const recipients = {}; - - // Reconstruct recipient objects - for (const key in data) { - const match = key.match(/^recipient\.(\d+)\.(.+)$/); - if (match) { - const index = match[1]; - const field = match[2]; - - if (!recipients[index]) { - recipients[index] = {}; - } - - recipients[index][field] = data[key]; - } - } - - // Loop through and print userName - for (const index in recipients) { - messageEl.innerHTML += `${recipients[index].userName}
`; - } - - contentEl.appendChild(messageEl); - } - - // Set the platform icon - SetPlatformIcon(iconEl, 'kick'); - } - break; - - // StreamElements Events - case ('StreamElementsTip'): - { - const avatarURL = await GetAvatar(data.tipUsername, 'twitch'); - if (IsValidUrl(avatarURL)) - avatarEl.src = avatarURL; - else - avatarEl.style.display = 'none' - titleEl.style.fontSize = '2em'; - titleEl.innerText = FormatCurrency(data.tipAmount, data.tipCurrency); - subtitleEl.innerText = `${data.tipUsername}`; - - if (data.tipMessage) { - const messageEl = document.createElement('div'); - messageEl.innerHTML = `${data.tipMessage}`; - - contentEl.appendChild(messageEl); - } - else { - contentEl.style.display = 'none'; - } - } - break; - - // Streamlabs Events - case ('StreamlabsDonation'): - { - const avatarURL = await GetAvatar(data.donationFrom, 'twitch'); - if (IsValidUrl(avatarURL)) - avatarEl.src = avatarURL; - else - avatarEl.style.display = 'none' - titleEl.style.fontSize = '2em'; - titleEl.innerText = data.donationFormattedAmount; - subtitleEl.innerText = `${data.donationFrom}`; - - if (data.donationMessage) { - const messageEl = document.createElement('div'); - messageEl.innerHTML = `${data.donationMessage}`; - - contentEl.appendChild(messageEl); - } - else { - contentEl.style.display = 'none'; - } - } - break; - - // Fourthwall Events - case ('FourthwallDonation'): - { - const avatarURL = await GetAvatar(data["fw.username"], 'twitch'); - if (IsValidUrl(avatarURL)) - avatarEl.src = avatarURL; - else - avatarEl.style.display = 'none' - titleEl.style.fontSize = '2em'; - titleEl.innerText = FormatCurrency(data["fw.amount"], data["fw.currency"]); - if (data["fw.username"]) - subtitleEl.innerText = `${data["fw.username"]}`; - else if (data["fw.email"]) - subtitleEl.innerText = `${data["fw.email"]}`; - - if (data["fw.message"]) { - const messageEl = document.createElement('div'); - messageEl.innerHTML = `${data["fw.message"]}`; - - contentEl.appendChild(messageEl); - } - else { - contentEl.style.display = 'none'; - } - } - break; - // case ('FourthwallGiftPurchase'): - // break; - case ('FourthwallOrderPlaced'): - { - // Only print non-free orders - if (data["fw.total"] <= 0) - return; - - const avatarURL = await GetAvatar(data["fw.username"], 'twitch'); - if (IsValidUrl(avatarURL)) - avatarEl.src = avatarURL; - else - avatarEl.style.display = 'none' - titleEl.style.fontSize = '2em'; - titleEl.innerText = FormatCurrency(data["fw.total"], data["fw.currency"]); - if (data["fw.username"]) - subtitleEl.innerText = `${data["fw.username"]}`; - else if (data["fw.email"]) - subtitleEl.innerText = `${data["fw.email"]}`; - - // Compile a list of all items bought - const variants = []; - - // Iterate through all keys in the data object - for (const key in data) { - const match = key.match(/^fw\.variants\[(\d+)\]\.(\w+)$/); - if (match) { - const index = Number(match[1]); - const field = match[2]; - - // Make sure the array slot exists - if (!variants[index]) { - variants[index] = {}; - } - - // Assign the field to the appropriate variant object - variants[index][field] = data[key]; - } - } - - // Print each item on the receipt - const messageEl = document.createElement('div'); - variants.forEach((variant, i) => { - messageEl.innerHTML += `${variant.quantity} × ${variant.name}
`; - }); - messageEl.style.textAlign = 'left'; - - // Check if they left a custom message - let customMessageEl = document.createElement('div'); - const customMessage = data["fw.statmessageus"]; - if (customMessage) - { - const txt = document.createElement("textarea"); - txt.innerHTML = customMessage; - customMessageEl.innerHTML += `
${txt.value}`; - } - - // Add a cute thank you message because you're uwu like that - const thankYouEl = document.createElement('div'); - thankYouEl.innerHTML += `
Thank you for your purchase!`; - - contentEl.appendChild(messageEl); - contentEl.appendChild(customMessageEl); - contentEl.appendChild(thankYouEl); - } - break; - case ('FourthwallSubscriptionPurchased'): - { - const avatarURL = await GetAvatar(data["fw.nickname"], 'twitch'); - if (IsValidUrl(avatarURL)) - avatarEl.src = avatarURL; - else - avatarEl.style.display = 'none' - titleEl.innerText = `New Member`; - subtitleEl.innerHTML = `${data["fw.nickname"]}`; - - const messageEl = document.createElement('div'); - messageEl.innerHTML = `Thanks for joining at the ${FormatCurrency(data["fw.amount"], data["fw.currency"])} tier!`; - - contentEl.appendChild(messageEl); - } - break; - - // Custom Code Events - case ('CustomCodeEvent'): - { - switch (data.triggerCustomCodeEventName) { - case ('kickIncomingRaid'): - { - avatarEl.src = ConvertWEBPToPNG(await GetAvatar(data.user, 'kick')); - - const messageEl = document.createElement('div'); - messageEl.innerHTML = `${data.user}
is hosting with a party of
${data.viewers} viewers!`; - - contentEl.appendChild(messageEl); - - // Set the platform icon - SetPlatformIcon(iconEl, 'kick'); - } - break; - } - } - break; - - // Don't print any event not excplicitly listed above - default: - return; - } - - // Set the timestamp - const { DateTime } = luxon; - const now = DateTime.local(); - const formatted = now.toFormat("cccc, LLLL d',' yyyy HH:mm:ss"); - - // Add ordinal suffix manually - function addOrdinal(n) { - if (n >= 11 && n <= 13) return 'th'; - switch (n % 10) { - case 1: return 'st'; - case 2: return 'nd'; - case 3: return 'rd'; - default: return 'th'; - } - } - - const day = now.day; - const ordinal = addOrdinal(day); - const fullFormatted = now.toFormat(`cccc, LLLL '${day}${ordinal}' yyyy HH:mm:ss`); - - dateEl.textContent = fullFormatted; - - // Send it to the print routine! - const receiptHTML = await GetRenderedHTML(instance); - console.log(receiptHTML); - client.doAction({ name: 'Printer Bot | Print Routine' }, { - receiptHTML: receiptHTML, - isTest: data.isTest - }); -} - - -////////////////////// -// HELPER FUNCTIONS // -////////////////////// - -async function GetAvatar(username, platform) { - - // First, check if the username is hashed already - if (avatarMap.has(`${username}-${platform}`)) { - console.debug(`Avatar found for ${username} (${platform}). Retrieving from hash map.`) - return avatarMap.get(`${username}-${platform}`); - } - - // If code reaches this point, the username hasn't been hashed, so retrieve avatar - switch (platform) { - case 'twitch': - { - console.debug(`No avatar found for ${username} (${platform}). Retrieving from Decapi.`) - let response = await fetch('https://decapi.me/twitch/avatar/' + username); - let data = await response.text(); - avatarMap.set(`${username}-${platform}`, data); - return data; - } - case 'kick': - { - console.debug(`No avatar found for ${username} (${platform}). Retrieving from Kick.`) - try { - let response = await fetch('https://kick.com/api/v2/channels/' + username); - console.log('https://kick.com/api/v2/channels/' + username) - let data = await response.json(); - let avatarURL = data.user.profile_pic; - if (!avatarURL) - avatarURL = 'https://kick.com/img/default-profile-pictures/default2.jpeg'; - avatarMap.set(`${username}-${platform}`, avatarURL); - return avatarURL; - } - catch (error) { - console.debug(error); - return 'https://kick.com/img/default-profile-pictures/default2.jpeg'; - } - } - } -} - -async function GetRenderedHTML(fragment) { - if (!(fragment instanceof DocumentFragment)) { - throw new Error('Argument must be a DocumentFragment'); - } - - // Filter out comment nodes from fragment content - const nodes = Array.from(fragment.childNodes).filter( - node => node.nodeType !== Node.COMMENT_NODE - ); - - const bodyContent = nodes - .map(node => node.outerHTML || node.textContent) - .join(''); - - // Get inline - - - ${bodyContent} - -`.trim(); - - return fullHTML; -} - -function ConvertWEBPToPNG(URL) { - return `https://images.weserv.nl/?url=${URL}&output=png`; -} - -function FormatCurrency(amount, currency) { - const isISOCode = /^[A-Z]{3}$/.test(currency); - - if (isISOCode) { - try { - return new Intl.NumberFormat(undefined, { - style: 'currency', - currency: currency, - currencyDisplay: 'symbol', - }).format(amount); - } catch { - return `${amount.toFixed(2)} ${currency}`; - } - } - - // Handle some common symbols that go before the number - const symbolsBefore = ['$', '€', '£', '¥', '₹']; - - if (symbolsBefore.includes(currency)) { - return `${currency}${amount.toFixed(2)}`; - } - - // Otherwise default to appending after - return `${amount.toFixed(2)} ${currency}`; -} - -function IsValidUrl(string) { - try { - new URL(string); - return true; - } catch { - return false; - } -} - -function EscapeRegExp(string) { - return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string -} - -function SetPlatformIcon(el, platform) { - // Set the platform icon - let baseURL = window.location.href; - baseURL = baseURL.replace(/index\.html$/i, ''); - - el.src = `${baseURL}/icons/platforms/${platform}.png`; -} - - - -/////////////////////////////////// -// STREAMER.BOT WEBSOCKET STATUS // -/////////////////////////////////// - -function SetConnectionState(isConnected) { - if (isConnected) { - document.getElementById('ip').disabled = true; - document.getElementById('port').disabled = true; - document.getElementById('connect-button').style.backgroundColor = '#e43b3b'; - document.getElementById('connect-button').innerText = 'Disconnect'; - - localStorage.setItem('sbServerAddress', document.getElementById('ip').value); - localStorage.setItem('sbServerPort', document.getElementById('port').value); - } - else { - document.getElementById('ip').disabled = false; - document.getElementById('port').disabled = false; - document.getElementById('connect-button').style.backgroundColor = '#3be477'; - document.getElementById('connect-button').innerText = 'Connect'; - } -} - -function Connect() { - if (document.getElementById('ip').disabled) - client.disconnect(); - else - { - client.options.host = document.getElementById('ip').value; - client.options.port = document.getElementById('port').value; - client.connect(); - } -} \ No newline at end of file +dockWrapper.addEventListener('load', () => { + dockWrapper.contentWindow.content.src = window.location.href + '/contents'; +}); \ No newline at end of file diff --git a/printer-bot/style.css b/printer-bot/style.css deleted file mode 100644 index fa6aad9..0000000 --- a/printer-bot/style.css +++ /dev/null @@ -1,127 +0,0 @@ -body { - font-size: 16px; - font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica, Arial, sans-serif; - text-align: center; - margin: 0; - padding: 0; -} - -#background { - color: white; - background-color: #282828; - display: flex; - justify-content: center; - align-items: center; - height: 100vh; -} - -label { - font-weight: 600; -} - -button { - font-size: 1em; - font-weight: 500; - background-color: #3be477; - color: white; - opacity: 0.8; - border-width: 0; - border-radius: 0.25em; - padding: 0.25em 0.5em; - width: 100%; -} - -button:hover { - opacity: 1; - cursor: pointer; -} - -button:disabled { - opacity: 0.5; - cursor: inherit; -} - -input { - border-radius: 0.5em; - width: calc(100% - 20px); - margin: 10px 0px; - padding: 10px 10px; - background-color: #ffffff05; - border-width: 0px; - color: white; - font-size: 1em; -} - -input:disabled { - opacity: 0.5; -} - -textarea:focus, -input:focus { - outline: none; -} - -#connect-box { - font-size: 24px; - background-color: #181818; - text-align: left; - display: flex; - flex-direction: column; - gap: 1em; - padding: 2em; - border-radius: 0.5em; -} - -.field { - display: flex; - flex-direction: column; -} - -#header {} - -#avatar { - width: 6em; - height: 6em; - border-radius: 50%; - object-fit: cover; -} - -#title { - font-weight: 900; - font-size: 1.5em; - text-transform: uppercase; -} - -#subtitle { - font-weight: 700; - font-size: 1.2em; -} - -#attribute { - padding: 0.5em 0em; -} - -#content { - /* border-top: 1px solid black; - border-bottom: 1px solid black; - border-left: none; - border-right: none; - margin: 1em 0em; */ - padding: 0.5em 0em; -} - -#icon { - height: 1em; -} - -#date { - margin: 0.5em 0em; - font-size: 0.7em; - text-transform: uppercase; -} - -#footer {} - -.emote { - height: 1em; -} \ No newline at end of file