Revert "Merge Beta to Main"

This commit is contained in:
nuttylmao
2025-09-22 04:39:20 +10:00
committed by GitHub
parent b7fcf2335b
commit 619cdb9c4c
51 changed files with 1486 additions and 3435 deletions
File diff suppressed because one or more lines are too long
Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

-76
View File
@@ -1,76 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<link rel="stylesheet" href="../../../.common/styles/global.css" />
<link rel="stylesheet" href="./style.css" />
<script src="https://cdn.jsdelivr.net/npm/luxon@3/build/global/luxon.min.js"></script>
</head>
<body>
<div id="settings">
<div class="setting">
<div>
<div class="setting-label" for="printer-name">Printer Name</div>
<div class="setting-description">As labelled in Windows > Settings > Printers & Scanners</div>
</div>
<input type="text" id="printer-name" name="printer-name" placeholder="" value="" autocomplete="off">
</div>
<div class="setting">
<div>
<div class="setting-label" for="paper-width">Paper Width (mm)</div>
<div class="setting-description">Printer Bot was designed for 80mm thermal paper</div>
</div>
<input type="number" id="paper-width" name="paper-width" placeholder="80" value="80" min="0" autocomplete="off">
</div>
<div class="setting">
<div>
<div class="setting-label" for="ignore-test-triggers">Ignore Test Triggers</div>
<div class="setting-description">In enabled, 'Test' and 'Simulate' events will be ignored</div>
</div>
<label class="switch" style="margin-left: auto;">
<input type="checkbox" id="ignore-test-triggers" name="ignore-test-triggers">
<span class="slider round"></span>
</label>
</div>
<div class="setting">
<div>
<div class="setting-label" for="delete-temp-files">Delete Temp Files</div>
<div class="setting-description">Turn this off for debugging</div>
</div>
<label class="switch" style="margin-left: auto;">
<input type="checkbox" id="delete-temp-files" name="delete-temp-files" checked>
<span class="slider round"></span>
</label>
</div>
<button id="test-print-button" onclick="TestPrint()">Test Print</button>
</div>
</body>
</html>
<!-- Receipt Template -->
<template id="receipt-template">
<div id="receipt-container">
<!-- Header -->
<div id="receipt-header">
<img id="receipt-avatar">
<div>
<div id="receipt-title"></div>
<div id="receipt-subtitle"></div>
</div>
</div>
<!-- Contents -->
<div id="receipt-content">
</div>
<!-- Footer -->
<div id="receipt-footer">
<img id="receipt-icon" src="">
<div id="receipt-date"></div>
</div>
</div>
</template>
<script src="script.js"></script>
-790
View File
@@ -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 = `<img src="${data.emotes[i].imageUrl}" class="emote"/>`;
const emoteName = EscapeRegExp(data.emotes[i].name);
let regexPattern = emoteName;
// Check if the emote name consists only of word characters (alphanumeric and underscore)
if (/^\w+$/.test(emoteName)) {
regexPattern = `\\b${emoteName}\\b`;
}
else {
// For non-word emotes, ensure they are surrounded by non-word characters or boundaries
regexPattern = `(?<=^|[^\\w])${emoteName}(?=$|[^\\w])`;
}
const regex = new RegExp(regexPattern, 'g');
messageEl.innerHTML = messageEl.innerHTML.replace(regex, emoteElement);
}
// Render cheermotes
for (i in data.cheerEmotes) {
const bits = data.cheerEmotes[i].bits;
const imageUrl = data.cheerEmotes[i].imageUrl;
const name = data.cheerEmotes[i].name;
const cheerEmoteElement = `<img src="${imageUrl}" class="emote"/>`;
const bitsElements = `<span class="bits">${bits}</span>`
messageEl.innerHTML = messageEl.innerHTML.replace(new RegExp(`\\b${name}${bits}\\b`, 'i'), cheerEmoteElement + bitsElements);
}
contentEl.appendChild(messageEl);
// Set the platform icon
SetPlatformIcon(iconEl, 'twitch');
}
break;
case ('TwitchSub'):
{
avatarEl.src = await GetAvatar(data.userName, 'twitch');
titleEl.innerText = `${data.tier} subscriber`;
subtitleEl.innerText = `${data.user}`;
const messageEl = document.createElement('div');
messageEl.innerHTML = '<b>First time subscriber!</b>';
contentEl.appendChild(messageEl);
// Set the platform icon
SetPlatformIcon(iconEl, 'twitch');
}
break;
case ('TwitchReSub'):
{
avatarEl.src = await GetAvatar(data.userName, 'twitch');
titleEl.innerText = `${data.tier} subscriber`;
subtitleEl.innerText = `${data.user}`;
const messageEl = document.createElement('div');
messageEl.innerHTML = `<b>${data.cumulative} months${data.monthStreak > 1 ? ' (' + data.monthStreak + ' in a row!)' : ''}</b>`;
if (data.messageStripped)
messageEl.innerHTML += `<br><br><i>${data.messageStripped}</i>`;
contentEl.appendChild(messageEl);
// Set the platform icon
SetPlatformIcon(iconEl, 'twitch');
}
break;
case ('TwitchGiftSub'):
{
// Don't put profile pictures for subs that come from Gift Bombs to avoid rate limits
if (data.fromGiftBomb)
//avatarEl.style.display = 'none';
return;
else
avatarEl.src = await GetAvatar(data.recipientUserName, 'twitch');
titleEl.innerText = `Gifted Sub`;
const messageEl = document.createElement('div');
if (data.anonymous)
messageEl.innerHTML += `<b>✦・゚ A mysterious admirer ・゚✦</b><br>`;
else
messageEl.innerHTML += `<b>${data.user}</b><br>`;
messageEl.innerHTML += `gifted a ${data.tier} sub to<br><b>${data.recipientUser}</b>`;
contentEl.appendChild(messageEl);
// Set the platform icon
SetPlatformIcon(iconEl, 'twitch');
}
break;
case ('TwitchGiftBomb'):
{
avatarEl.src = await GetAvatar(data.userName, 'twitch');
titleEl.innerHTML = `${data.gifts} × Gifted Subs`;
subtitleEl.innerHTML += `✧*・゚✧ ${data.tier.toUpperCase()} ✧・゚*✧`;
if (data.anonymous)
subtitleEl.innerHTML += `<br>From a mystery person...`;
else
subtitleEl.innerHTML += `<br>${data.user}`;
const messageEl = document.createElement('div');
if (data.totalGifts > 1) {
messageEl.innerHTML = `They've gifted <b>${data.totalGifts} subs</b> in total!</br></br>`;
}
// Get a list of all recipient users
Object.keys(data)
.filter(key => /^gift\.recipientUser\d+$/.test(key))
.forEach((key, index) => {
const username = data[key];
messageEl.innerHTML += `${username}</br>`;
});
contentEl.appendChild(messageEl);
// Set the platform icon
SetPlatformIcon(iconEl, 'twitch');
}
break;
case ('TwitchRaid'):
{
avatarEl.src = await GetAvatar(data.userName, 'twitch');
const messageEl = document.createElement('div');
messageEl.innerHTML = `<b>${data.user}</b><br>is raiding with a party of<br><b>${data.viewers} viewers!</b>`;
contentEl.appendChild(messageEl);
// Set the platform icon
SetPlatformIcon(iconEl, 'twitch');
}
break;
// YouTube Events
case ('YouTubeNewSponsor'):
{
if (data.userProfileUrl)
avatarEl.src = data.userProfileUrl;
else
avatarEl.style.display = 'none';
titleEl.innerText = `${data.levelName}`;
subtitleEl.innerText = `${data.user}`;
contentEl.style.display = 'none';
// Set the platform icon
SetPlatformIcon(iconEl, 'youtube');
}
break;
case ('YouTubeGiftMembershipReceived'):
{
if (data.gifterProfileUrl)
avatarEl.src = data.gifterProfileUrl;
else
avatarEl.style.display = 'none';
titleEl.innerText = `Gifted Membership`;
const messageEl = document.createElement('div');
messageEl.innerHTML = `<b>${data.gifterUser}</b><br>gifted a membership to<br><b>${data.user}</b>!`;
contentEl.appendChild(messageEl);
// Set the platform icon
SetPlatformIcon(iconEl, 'youtube');
}
break;
case ('YouTubeSuperChat'):
{
if (data.userProfileUrl)
avatarEl.src = data.userProfileUrl;
else
avatarEl.style.display = 'none';
titleEl.style.fontSize = '2em';
titleEl.innerText = `${data.amount}`;
const messageEl = document.createElement('div');
messageEl.innerHTML = `<b>${data.user}</b><br>sent a Super Chat!`;
if (data.message)
messageEl.innerHTML += `<br><br><i>${data.message}</i>`;
contentEl.appendChild(messageEl);
// Set the platform icon
SetPlatformIcon(iconEl, 'youtube');
}
break;
case ('YouTubeSuperSticker'):
{
if (data.stickerImageUrl)
avatarEl.src = data.stickerImageUrl;
else
avatarEl.style.display = 'none';
titleEl.style.fontSize = '2em';
titleEl.innerText = `${data.amount}`;
const messageEl = document.createElement('div');
messageEl.innerHTML = `<b>${data.user}</b><br>sent a Super Sticker!`;
contentEl.appendChild(messageEl);
// Set the platform icon
SetPlatformIcon(iconEl, 'youtube');
}
break;
break;
// Kick Events
case ('KickSubscription'):
case ('KickResubscription'):
{
avatarEl.src = ConvertWEBPToPNG(await GetAvatar(data.user, 'kick'));
titleEl.innerText = `Subscriber`;
subtitleEl.innerText = `${data.user}`;
const messageEl = document.createElement('div');
if (data.duration > 1)
messageEl.innerHTML = `<b>${data.duration} months</b>`;
else
messageEl.innerHTML = '<b>First time subscriber!</b>';
contentEl.appendChild(messageEl);
// Set the platform icon
SetPlatformIcon(iconEl, 'kick');
}
break;
case ('KickGiftSubscription'):
{
avatarEl.src = ConvertWEBPToPNG(await GetAvatar(data["recipient.userLogin"], 'kick'));
titleEl.innerText = `Gifted Sub`;
const messageEl = document.createElement('div');
messageEl.innerHTML = `<b>${data.user}</b><br>gifted a sub to<br><b>${data["recipient.userName"]}</b>!`;
contentEl.appendChild(messageEl);
// Set the platform icon
SetPlatformIcon(iconEl, 'kick');
}
break;
case ('KickMassGiftSubscription'):
{
// There is only one sub, so use the same template for a single gifted sub
if ('recipient.userName' in data) {
avatarEl.src = ConvertWEBPToPNG(await GetAvatar(data["recipient.userLogin"], 'kick'));
titleEl.innerText = `Gifted Sub`;
const messageEl = document.createElement('div');
messageEl.innerHTML = `<b>${data.user}</b><br>gifted a sub to<br><b>${data["recipient.userName"]}</b>!`;
contentEl.appendChild(messageEl);
}
else {
avatarEl.src = ConvertWEBPToPNG(await GetAvatar(data.user, 'kick'));
// Calculate how many subs were gived
let maxIndex = -1;
for (const key in data) {
const match = key.match(/^recipient\.(\d+)\./);
if (match) {
const index = parseInt(match[1], 10);
if (index > maxIndex) {
maxIndex = index;
}
}
}
const totalGifts = maxIndex + 1;
titleEl.innerHTML = `${totalGifts} × Gifted Subs`;
subtitleEl.innerText = `${data.user}`;
const messageEl = document.createElement('div');
// Loop through each recipient and include it in the receipt
const recipients = {};
// Reconstruct recipient objects
for (const key in data) {
const match = key.match(/^recipient\.(\d+)\.(.+)$/);
if (match) {
const index = match[1];
const field = match[2];
if (!recipients[index]) {
recipients[index] = {};
}
recipients[index][field] = data[key];
}
}
// Loop through and print userName
for (const index in recipients) {
messageEl.innerHTML += `${recipients[index].userName}<br>`;
}
contentEl.appendChild(messageEl);
}
// Set the platform icon
SetPlatformIcon(iconEl, 'kick');
}
break;
// StreamElements Events
case ('StreamElementsTip'):
{
const avatarURL = await GetAvatar(data.tipUsername, 'twitch');
if (IsValidUrl(avatarURL))
avatarEl.src = avatarURL;
else
avatarEl.style.display = 'none'
titleEl.style.fontSize = '2em';
titleEl.innerText = FormatCurrency(data.tipAmount, data.tipCurrency);
subtitleEl.innerText = `${data.tipUsername}`;
if (data.tipMessage) {
const messageEl = document.createElement('div');
messageEl.innerHTML = `<i>${data.tipMessage}</i>`;
contentEl.appendChild(messageEl);
}
else {
contentEl.style.display = 'none';
}
}
break;
// Streamlabs Events
case ('StreamlabsDonation'):
{
const avatarURL = await GetAvatar(data.donationFrom, 'twitch');
if (IsValidUrl(avatarURL))
avatarEl.src = avatarURL;
else
avatarEl.style.display = 'none'
titleEl.style.fontSize = '2em';
titleEl.innerText = data.donationFormattedAmount;
subtitleEl.innerText = `${data.donationFrom}`;
if (data.donationMessage) {
const messageEl = document.createElement('div');
messageEl.innerHTML = `<i>${data.donationMessage}</i>`;
contentEl.appendChild(messageEl);
}
else {
contentEl.style.display = 'none';
}
}
break;
// Fourthwall Events
case ('FourthwallDonation'):
{
const avatarURL = await GetAvatar(data["fw.username"], 'twitch');
if (IsValidUrl(avatarURL))
avatarEl.src = avatarURL;
else
avatarEl.style.display = 'none'
titleEl.style.fontSize = '2em';
titleEl.innerText = FormatCurrency(data["fw.amount"], data["fw.currency"]);
if (data["fw.username"])
subtitleEl.innerText = `${data["fw.username"]}`;
else if (data["fw.email"])
subtitleEl.innerText = `${data["fw.email"]}`;
if (data["fw.message"]) {
const messageEl = document.createElement('div');
messageEl.innerHTML = `<i>${data["fw.message"]}</i>`;
contentEl.appendChild(messageEl);
}
else {
contentEl.style.display = 'none';
}
}
break;
// case ('FourthwallGiftPurchase'):
// break;
case ('FourthwallOrderPlaced'):
{
// Only print non-free orders
if (data["fw.total"] <= 0)
return;
const avatarURL = await GetAvatar(data["fw.username"], 'twitch');
if (IsValidUrl(avatarURL))
avatarEl.src = avatarURL;
else
avatarEl.style.display = 'none'
titleEl.style.fontSize = '2em';
titleEl.innerText = FormatCurrency(data["fw.total"], data["fw.currency"]);
if (data["fw.username"])
subtitleEl.innerText = `${data["fw.username"]}`;
else if (data["fw.email"])
subtitleEl.innerText = `${data["fw.email"]}`;
// Compile a list of all items bought
const variants = [];
// Iterate through all keys in the data object
for (const key in data) {
const match = key.match(/^fw\.variants\[(\d+)\]\.(\w+)$/);
if (match) {
const index = Number(match[1]);
const field = match[2];
// Make sure the array slot exists
if (!variants[index]) {
variants[index] = {};
}
// Assign the field to the appropriate variant object
variants[index][field] = data[key];
}
}
// Print each item on the receipt
const messageEl = document.createElement('div');
variants.forEach((variant, i) => {
messageEl.innerHTML += `${variant.quantity} × ${variant.name}<br>`;
});
messageEl.style.textAlign = 'left';
// Check if they left a custom message
let customMessageEl = document.createElement('div');
const customMessage = data["fw.statmessageus"];
if (customMessage) {
const txt = document.createElement("textarea");
txt.innerHTML = customMessage;
customMessageEl.innerHTML += `<br><i>${txt.value}</i>`;
}
// Add a cute thank you message because you're uwu like that
const thankYouEl = document.createElement('div');
thankYouEl.innerHTML += `<br><b>Thank you for your purchase!</b>`;
contentEl.appendChild(messageEl);
contentEl.appendChild(customMessageEl);
contentEl.appendChild(thankYouEl);
}
break;
case ('FourthwallSubscriptionPurchased'):
{
const avatarURL = await GetAvatar(data["fw.nickname"], 'twitch');
if (IsValidUrl(avatarURL))
avatarEl.src = avatarURL;
else
avatarEl.style.display = 'none'
titleEl.innerText = `New Member`;
subtitleEl.innerHTML = `${data["fw.nickname"]}`;
const messageEl = document.createElement('div');
messageEl.innerHTML = `Thanks for joining at the <b>${FormatCurrency(data["fw.amount"], data["fw.currency"])}</b> tier!`;
contentEl.appendChild(messageEl);
}
break;
// Custom Code Events
case ('CustomCodeEvent'):
{
switch (data.triggerCustomCodeEventName) {
case ('kickIncomingRaid'):
{
avatarEl.src = ConvertWEBPToPNG(await GetAvatar(data.user, 'kick'));
const messageEl = document.createElement('div');
messageEl.innerHTML = `<b>${data.user}</b><br>is hosting with a party of<br><b>${data.viewers} viewers!</b>`;
contentEl.appendChild(messageEl);
// Set the platform icon
SetPlatformIcon(iconEl, 'kick');
}
break;
}
}
break;
// Don't print any event not excplicitly listed above
default:
return;
}
// Set the timestamp
const { DateTime } = luxon;
const now = DateTime.local();
const formatted = now.toFormat("cccc, LLLL d',' yyyy HH:mm:ss");
// Add ordinal suffix manually
function addOrdinal(n) {
if (n >= 11 && n <= 13) return 'th';
switch (n % 10) {
case 1: return 'st';
case 2: return 'nd';
case 3: return 'rd';
default: return 'th';
}
}
const day = now.day;
const ordinal = addOrdinal(day);
const fullFormatted = now.toFormat(`cccc, LLLL '${day}${ordinal}' yyyy HH:mm:ss`);
dateEl.textContent = fullFormatted;
// Send it to the print routine!
const receiptHTML = await GetRenderedHTML(instance);
window.parent.sbClient.doAction({ id: sbActionPrintRoutine }, {
receiptHTML: receiptHTML,
isTest: data.isTest,
printerName: document.getElementById('printer-name').value,
paperWidth: document.getElementById('paper-width').value,
ignoreTestTriggers: document.getElementById('ignore-test-triggers').checked,
deleteTempFiles: document.getElementById('delete-temp-files').checked
});
}
//////////////////////
// HELPER FUNCTIONS //
//////////////////////
async function GetAvatar(username, platform) {
// First, check if the username is hashed already
if (avatarMap.has(`${username}-${platform}`)) {
console.debug(`Avatar found for ${username} (${platform}). Retrieving from hash map.`)
return avatarMap.get(`${username}-${platform}`);
}
// If code reaches this point, the username hasn't been hashed, so retrieve avatar
switch (platform) {
case 'twitch':
{
console.debug(`No avatar found for ${username} (${platform}). Retrieving from Decapi.`)
let response = await fetch('https://decapi.me/twitch/avatar/' + username);
let data = await response.text();
avatarMap.set(`${username}-${platform}`, data);
return data;
}
case 'kick':
{
console.debug(`No avatar found for ${username} (${platform}). Retrieving from Kick.`)
try {
let response = await fetch('https://kick.com/api/v2/channels/' + username);
console.log('https://kick.com/api/v2/channels/' + username)
let data = await response.json();
let avatarURL = data.user.profile_pic;
if (!avatarURL)
avatarURL = 'https://kick.com/img/default-profile-pictures/default2.jpeg';
avatarMap.set(`${username}-${platform}`, avatarURL);
return avatarURL;
}
catch (error) {
console.debug(error);
return 'https://kick.com/img/default-profile-pictures/default2.jpeg';
}
}
}
}
async function GetRenderedHTML(fragment) {
if (!(fragment instanceof DocumentFragment)) {
throw new Error('Argument must be a DocumentFragment');
}
// Filter out comment nodes from fragment content
const nodes = Array.from(fragment.childNodes).filter(
node => node.nodeType !== Node.COMMENT_NODE
);
const bodyContent = nodes
.map(node => node.outerHTML || node.textContent)
.join('');
// Get inline <style> contents
const inlineStyles = Array.from(document.querySelectorAll('style'))
.map(style => style.textContent)
.join('\n');
// Get all external stylesheet URLs
const linkHrefs = Array.from(document.querySelectorAll('link[rel="stylesheet"]'))
.map(link => link.href);
// Fetch external CSS contents
const externalCSSContents = await Promise.all(
linkHrefs.map(async href => {
try {
const res = await fetch(href);
if (!res.ok) throw new Error(`Failed to load CSS from ${href}`);
return await res.text();
} catch {
console.warn(`Could not fetch CSS from ${href}`);
return '';
}
})
);
// Combine all CSS into one string
const combinedCSS = inlineStyles + '\n' + externalCSSContents.join('\n');
// Build the full standalone HTML string
const fullHTML = `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<style>${combinedCSS}</style>
</head>
<body>
${bodyContent}
</body>
</html>`.trim();
return fullHTML;
}
function ConvertWEBPToPNG(URL) {
return `https://images.weserv.nl/?url=${URL}&output=png`;
}
function FormatCurrency(amount, currency) {
const isISOCode = /^[A-Z]{3}$/.test(currency);
if (isISOCode) {
try {
return new Intl.NumberFormat(undefined, {
style: 'currency',
currency: currency,
currencyDisplay: 'symbol',
}).format(amount);
} catch {
return `${amount.toFixed(2)} ${currency}`;
}
}
// Handle some common symbols that go before the number
const symbolsBefore = ['$', '€', '£', '¥', '₹'];
if (symbolsBefore.includes(currency)) {
return `${currency}${amount.toFixed(2)}`;
}
// Otherwise default to appending after
return `${amount.toFixed(2)} ${currency}`;
}
function IsValidUrl(string) {
try {
new URL(string);
return true;
} catch {
return false;
}
}
function EscapeRegExp(string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
}
function SetPlatformIcon(el, platform) {
// Set the platform icon
let baseURL = window.location.href;
baseURL = baseURL.replace(/index\.html$/i, '');
el.src = `${baseURL}/icons/platforms/${platform}.png`;
}
///////////////////////
// PAGE INTERACTIONS //
///////////////////////
let data = {
"__source": "TwitchSub",
"tier": "prime",
"isPrimeSub": true,
"monthsSubscribed": 1,
"isTest": false,
"actionName": "Printer Bot | Events",
"user": "nutty",
"userName": "nutty",
"userType": "twitch"
}
async function TestPrint() {
CustomEvent(data);
}
///////////////////
// PAGE SETTINGS //
///////////////////
// Get references
const printerNameInput = document.getElementById('printer-name');
const paperWidthInput = document.getElementById('paper-width');
const ignoreTestTriggersInput = document.getElementById('ignore-test-triggers');
const deleteTempFilesInput = document.getElementById('delete-temp-files');
// Local storage key must be prefixed with the first URL segment
const currentPath = window.location.pathname;
const urlSegment = currentPath.split('/').filter(Boolean)[0];
const storageKey = (id) => `${urlSegment}::${id}`;
function saveSetting(id) {
const el = document.getElementById(id);
const value = el.type === "checkbox" ? el.checked : el.value;
localStorage.setItem(storageKey(id), value);
}
// Add event listeners
[printerNameInput, paperWidthInput, ignoreTestTriggersInput, deleteTempFilesInput].forEach(input => {
input.addEventListener("input", () => saveSetting(input.id));
input.addEventListener("change", () => saveSetting(input.id));
});
// Load settings
if (localStorage.getItem(storageKey(printerNameInput.id)))
printerNameInput.value = localStorage.getItem(storageKey(printerNameInput.id));
if (localStorage.getItem(storageKey(paperWidthInput.id)))
paperWidthInput.value = localStorage.getItem(storageKey(paperWidthInput.id));
if (localStorage.getItem(storageKey(ignoreTestTriggersInput.id)))
ignoreTestTriggersInput.checked = JSON.parse(localStorage.getItem(storageKey(ignoreTestTriggersInput.id)));
if (localStorage.getItem(storageKey(deleteTempFilesInput.id)))
deleteTempFilesInput.checked = JSON.parse(localStorage.getItem(storageKey(deleteTempFilesInput.id)));
-78
View File
@@ -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;
}

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

Before

Width:  |  Height:  |  Size: 8.4 KiB

After

Width:  |  Height:  |  Size: 8.4 KiB

+42 -13
View File
@@ -1,20 +1,49 @@
<!DOCTYPE html>
<head>
<meta charset="UTF-8">
<link rel="stylesheet" href="../.common/styles/global.css" />
<link rel="icon" href="../../.resources/logo.png" type="image/png">
<title>nutty</title>
<style>
#dock-wrapper {
width: 100vw;
height: 100vh;
}
</style>
<meta charset="UTF-8">
<link rel="shortcut icon" href="#" />
<link rel="stylesheet" href="./style.css" />
<script type="text/javascript" src="https://unpkg.com/@streamerbot/client/dist/streamerbot-client.js"></script>
<script src="https://cdn.jsdelivr.net/npm/luxon@3/build/global/luxon.min.js"></script>
</head>
<body>
<iframe id="dock-wrapper"></iframe>
<script src="script.js"></script>
<div id="background">
<div id="connect-box">
<div class="field">
<label for="ip">IP Address</label>
<input type="text" id="ip" name="ip" placeholder="127.0.0.1" value="127.0.0.1">
</div>
<div class="field">
<label for="port">Port</label>
<input type="text" id="port" name="port" placeholder="8080" value="8080">
</div>
<button id="connect-button" onclick="Connect()">Connect</button>
</div>
</div>
</body>
</html>
<template id="receipt-template">
<!-- Header -->
<div id="header">
<img id="avatar">
<div>
<div id="title"></div>
<div id="subtitle"></div>
</div>
</div>
<!-- Contents -->
<div id="content">
</div>
<!-- Footer -->
<div id="footer">
<div id="date"></div>
<img id="icon" src="icons/platforms/twitch.png">
</div>
</template>
<script src="./script.js"></script>
+800 -12
View File
@@ -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';
});
//////////////////////
// GLOBAL VARIABLES //
//////////////////////
const avatarMap = new Map();
/////////////////////////
// STREAMER.BOT CLIENT //
/////////////////////////
// Check local storage
if (localStorage.getItem('sbServerAddress') === null)
localStorage.setItem('sbServerAddress', '127.0.0.1');
if (localStorage.getItem('sbServerPort') === null)
localStorage.setItem('sbServerPort', '8080');
document.getElementById('ip').value = localStorage.getItem('sbServerAddress');
document.getElementById('port').value = localStorage.getItem('sbServerPort');
let sbServerAddress = document.getElementById('ip').value;
let sbServerPort = document.getElementById('port').value;
let client = new StreamerbotClient({
host: sbServerAddress,
port: sbServerPort,
onConnect: (data) => {
console.log(`Streamer.bot successfully connected to ${sbServerAddress}:${sbServerPort}`)
console.debug(data);
SetConnectionState(true);
},
onDisconnect: () => {
console.error(`Streamer.bot disconnected from ${sbServerAddress}:${sbServerPort}`)
SetConnectionState(false);
}
});
client.on('General.Custom', (response) => {
console.debug(response.data);
CustomEvent(response.data);
})
////////////////////
// STREAM PRINTER //
////////////////////
async function CustomEvent(data) {
if (data.actionName != 'Printer Bot | Events')
return;
// Get a reference to the template
const template = document.getElementById('receipt-template');
// Create a new instance of the template
const instance = template.content.cloneNode(true);
// Get divs
const headerEl = instance.querySelector('#header');
const contentEl = instance.querySelector('#content');
const footerEl = instance.querySelector('#footer');
const avatarEl = instance.querySelector('#avatar');
const titleEl = instance.querySelector('#title');
const subtitleEl = instance.querySelector('#subtitle');
const iconEl = instance.querySelector('#icon');
const dateEl = instance.querySelector('#date');
// Set the main contents
switch (data.__source) {
// Twitch events
case ('TwitchCheer'):
{
avatarEl.src = await GetAvatar(data.userName, 'twitch');
titleEl.innerText = `${data.bits} BITS`;
subtitleEl.innerText = `${data.user}`;
const messageEl = document.createElement('div');
messageEl.innerHTML = data.message;
// Render emotes
for (i in data.emotes) {
const emoteElement = `<img src="${data.emotes[i].imageUrl}" class="emote"/>`;
const emoteName = EscapeRegExp(data.emotes[i].name);
let regexPattern = emoteName;
// Check if the emote name consists only of word characters (alphanumeric and underscore)
if (/^\w+$/.test(emoteName)) {
regexPattern = `\\b${emoteName}\\b`;
}
else {
// For non-word emotes, ensure they are surrounded by non-word characters or boundaries
regexPattern = `(?<=^|[^\\w])${emoteName}(?=$|[^\\w])`;
}
const regex = new RegExp(regexPattern, 'g');
messageEl.innerHTML = messageEl.innerHTML.replace(regex, emoteElement);
}
// Render cheermotes
for (i in data.cheerEmotes) {
const bits = data.cheerEmotes[i].bits;
const imageUrl = data.cheerEmotes[i].imageUrl;
const name = data.cheerEmotes[i].name;
const cheerEmoteElement = `<img src="${imageUrl}" class="emote"/>`;
const bitsElements = `<span class="bits">${bits}</span>`
messageEl.innerHTML = messageEl.innerHTML.replace(new RegExp(`\\b${name}${bits}\\b`, 'i'), cheerEmoteElement + bitsElements);
}
contentEl.appendChild(messageEl);
// Set the platform icon
SetPlatformIcon(iconEl, 'twitch');
}
break;
case ('TwitchSub'):
{
avatarEl.src = await GetAvatar(data.userName, 'twitch');
titleEl.innerText = `${data.tier} subscriber`;
subtitleEl.innerText = `${data.user}`;
const messageEl = document.createElement('div');
messageEl.innerHTML = '<b>First time subscriber!</b>';
contentEl.appendChild(messageEl);
// Set the platform icon
SetPlatformIcon(iconEl, 'twitch');
}
break;
case ('TwitchReSub'):
{
avatarEl.src = await GetAvatar(data.userName, 'twitch');
titleEl.innerText = `${data.tier} subscriber`;
subtitleEl.innerText = `${data.user}`;
const messageEl = document.createElement('div');
messageEl.innerHTML = `<b>${data.cumulative} months${data.monthStreak > 1 ? ' (' + data.monthStreak + ' in a row!)' : ''}</b>`;
if (data.messageStripped)
messageEl.innerHTML += `<br><br><i>${data.messageStripped}</i>`;
contentEl.appendChild(messageEl);
// Set the platform icon
SetPlatformIcon(iconEl, 'twitch');
}
break;
case ('TwitchGiftSub'):
{
// Don't put profile pictures for subs that come from Gift Bombs to avoid rate limits
if (data.fromGiftBomb)
//avatarEl.style.display = 'none';
return;
else
avatarEl.src = await GetAvatar(data.recipientUserName, 'twitch');
titleEl.innerText = `Gifted Sub`;
const messageEl = document.createElement('div');
messageEl.innerHTML += `<b>${data.recipientUser}</b><br>received a ${data.tier} sub from<br>`;
if (data.anonymous)
messageEl.innerHTML += `a mysterious admirer...`;
else
messageEl.innerHTML += `<b>${data.user}</b>!`;
contentEl.appendChild(messageEl);
// Set the platform icon
SetPlatformIcon(iconEl, 'twitch');
}
break;
case ('TwitchGiftBomb'):
{
avatarEl.src = await GetAvatar(data.userName, 'twitch');
titleEl.innerHTML = `${data.gifts} × Gifted Subs`;
subtitleEl.innerHTML += `✧*・゚✧ ${data.tier.toUpperCase()} ✧・゚*✧`;
if (data.anonymous)
subtitleEl.innerHTML += `<br>From a mystery person...`;
else
subtitleEl.innerHTML += `<br>${data.user}`;
const messageEl = document.createElement('div');
if (data.totalGifts > 1) {
messageEl.innerHTML = `They've gifted <b>${data.totalGifts} subs</b> in total!</br></br>`;
}
// Get a list of all recipient users
Object.keys(data)
.filter(key => /^gift\.recipientUser\d+$/.test(key))
.forEach((key, index) => {
const username = data[key];
messageEl.innerHTML += `${username}</br>`;
});
contentEl.appendChild(messageEl);
// Set the platform icon
SetPlatformIcon(iconEl, 'twitch');
}
break;
case ('TwitchRaid'):
{
avatarEl.src = await GetAvatar(data.userName, 'twitch');
const messageEl = document.createElement('div');
messageEl.innerHTML = `<b>${data.user}</b><br>is raiding with a party of<br><b>${data.viewers} viewers!</b>`;
contentEl.appendChild(messageEl);
// Set the platform icon
SetPlatformIcon(iconEl, 'twitch');
}
break;
// YouTube Events
case ('YouTubeNewSponsor'):
{
if (data.userProfileUrl)
avatarEl.src = data.userProfileUrl;
else
avatarEl.style.display = 'none';
titleEl.innerText = `${data.levelName}`;
subtitleEl.innerText = `${data.user}`;
contentEl.style.display = 'none';
// Set the platform icon
SetPlatformIcon(iconEl, 'youtube');
}
break;
case ('YouTubeGiftMembershipReceived'):
{
if (data.gifterProfileUrl)
avatarEl.src = data.gifterProfileUrl;
else
avatarEl.style.display = 'none';
titleEl.innerText = `Gifted Membership`;
const messageEl = document.createElement('div');
messageEl.innerHTML = `<b>${data.user}</b><br>received a membership from<br><b>${data.gifterUser}</b>!`;
contentEl.appendChild(messageEl);
// Set the platform icon
SetPlatformIcon(iconEl, 'youtube');
}
break;
case ('YouTubeSuperChat'):
{
if (data.userProfileUrl)
avatarEl.src = data.userProfileUrl;
else
avatarEl.style.display = 'none';
titleEl.style.fontSize = '2em';
titleEl.innerText = `${data.amount}`;
const messageEl = document.createElement('div');
messageEl.innerHTML = `<b>${data.user}</b><br>sent a Super Chat!`;
if (data.message)
messageEl.innerHTML += `<br><br><i>${data.message}</i>`;
contentEl.appendChild(messageEl);
// Set the platform icon
SetPlatformIcon(iconEl, 'youtube');
}
break;
case ('YouTubeSuperSticker'):
{
if (data.stickerImageUrl)
avatarEl.src = data.stickerImageUrl;
else
avatarEl.style.display = 'none';
titleEl.style.fontSize = '2em';
titleEl.innerText = `${data.amount}`;
const messageEl = document.createElement('div');
messageEl.innerHTML = `<b>${data.user}</b><br>sent a Super Sticker!`;
contentEl.appendChild(messageEl);
// Set the platform icon
SetPlatformIcon(iconEl, 'youtube');
}
break;
break;
// Kick Events
case ('KickSubscription'):
case ('KickResubscription'):
{
avatarEl.src = ConvertWEBPToPNG(await GetAvatar(data.user, 'kick'));
titleEl.innerText = `Subscriber`;
subtitleEl.innerText = `${data.user}`;
const messageEl = document.createElement('div');
if (data.duration > 1)
messageEl.innerHTML = `<b>${data.duration} months</b>`;
else
messageEl.innerHTML = '<b>First time subscriber!</b>';
contentEl.appendChild(messageEl);
// Set the platform icon
SetPlatformIcon(iconEl, 'kick');
}
break;
case ('KickGiftSubscription'):
{
avatarEl.src = ConvertWEBPToPNG(await GetAvatar(data["recipient.userLogin"], 'kick'));
titleEl.innerText = `Gifted Sub`;
const messageEl = document.createElement('div');
messageEl.innerHTML = `<b>${data["recipient.userName"]}</b><br>received a sub from<br><b>${data.user}</b>!`;
contentEl.appendChild(messageEl);
// Set the platform icon
SetPlatformIcon(iconEl, 'kick');
}
break;
case ('KickMassGiftSubscription'):
{
// There is only one sub, so use the same template for a single gifted sub
if ('recipient.userName' in data) {
avatarEl.src = ConvertWEBPToPNG(await GetAvatar(data["recipient.userLogin"], 'kick'));
titleEl.innerText = `Gifted Sub`;
const messageEl = document.createElement('div');
messageEl.innerHTML = `<b>${data["recipient.userName"]}</b><br>received a sub from<br><b>${data.user}</b>!`;
contentEl.appendChild(messageEl);
}
else {
avatarEl.src = ConvertWEBPToPNG(await GetAvatar(data.user, 'kick'));
// Calculate how many subs were gived
let maxIndex = -1;
for (const key in data) {
const match = key.match(/^recipient\.(\d+)\./);
if (match) {
const index = parseInt(match[1], 10);
if (index > maxIndex) {
maxIndex = index;
}
}
}
const totalGifts = maxIndex + 1;
titleEl.innerHTML = `${totalGifts} × Gifted Subs`;
subtitleEl.innerText = `${data.user}`;
const messageEl = document.createElement('div');
// Loop through each recipient and include it in the receipt
const recipients = {};
// Reconstruct recipient objects
for (const key in data) {
const match = key.match(/^recipient\.(\d+)\.(.+)$/);
if (match) {
const index = match[1];
const field = match[2];
if (!recipients[index]) {
recipients[index] = {};
}
recipients[index][field] = data[key];
}
}
// Loop through and print userName
for (const index in recipients) {
messageEl.innerHTML += `${recipients[index].userName}<br>`;
}
contentEl.appendChild(messageEl);
}
// Set the platform icon
SetPlatformIcon(iconEl, 'kick');
}
break;
// StreamElements Events
case ('StreamElementsTip'):
{
const avatarURL = await GetAvatar(data.tipUsername, 'twitch');
if (IsValidUrl(avatarURL))
avatarEl.src = avatarURL;
else
avatarEl.style.display = 'none'
titleEl.style.fontSize = '2em';
titleEl.innerText = FormatCurrency(data.tipAmount, data.tipCurrency);
subtitleEl.innerText = `${data.tipUsername}`;
if (data.tipMessage) {
const messageEl = document.createElement('div');
messageEl.innerHTML = `<i>${data.tipMessage}</i>`;
contentEl.appendChild(messageEl);
}
else {
contentEl.style.display = 'none';
}
}
break;
// Streamlabs Events
case ('StreamlabsDonation'):
{
const avatarURL = await GetAvatar(data.donationFrom, 'twitch');
if (IsValidUrl(avatarURL))
avatarEl.src = avatarURL;
else
avatarEl.style.display = 'none'
titleEl.style.fontSize = '2em';
titleEl.innerText = data.donationFormattedAmount;
subtitleEl.innerText = `${data.donationFrom}`;
if (data.donationMessage) {
const messageEl = document.createElement('div');
messageEl.innerHTML = `<i>${data.donationMessage}</i>`;
contentEl.appendChild(messageEl);
}
else {
contentEl.style.display = 'none';
}
}
break;
// Fourthwall Events
case ('FourthwallDonation'):
{
const avatarURL = await GetAvatar(data["fw.username"], 'twitch');
if (IsValidUrl(avatarURL))
avatarEl.src = avatarURL;
else
avatarEl.style.display = 'none'
titleEl.style.fontSize = '2em';
titleEl.innerText = FormatCurrency(data["fw.amount"], data["fw.currency"]);
if (data["fw.username"])
subtitleEl.innerText = `${data["fw.username"]}`;
else if (data["fw.email"])
subtitleEl.innerText = `${data["fw.email"]}`;
if (data["fw.message"]) {
const messageEl = document.createElement('div');
messageEl.innerHTML = `<i>${data["fw.message"]}</i>`;
contentEl.appendChild(messageEl);
}
else {
contentEl.style.display = 'none';
}
}
break;
// case ('FourthwallGiftPurchase'):
// break;
case ('FourthwallOrderPlaced'):
{
// Only print non-free orders
if (data["fw.total"] <= 0)
return;
const avatarURL = await GetAvatar(data["fw.username"], 'twitch');
if (IsValidUrl(avatarURL))
avatarEl.src = avatarURL;
else
avatarEl.style.display = 'none'
titleEl.style.fontSize = '2em';
titleEl.innerText = FormatCurrency(data["fw.total"], data["fw.currency"]);
if (data["fw.username"])
subtitleEl.innerText = `${data["fw.username"]}`;
else if (data["fw.email"])
subtitleEl.innerText = `${data["fw.email"]}`;
// Compile a list of all items bought
const variants = [];
// Iterate through all keys in the data object
for (const key in data) {
const match = key.match(/^fw\.variants\[(\d+)\]\.(\w+)$/);
if (match) {
const index = Number(match[1]);
const field = match[2];
// Make sure the array slot exists
if (!variants[index]) {
variants[index] = {};
}
// Assign the field to the appropriate variant object
variants[index][field] = data[key];
}
}
// Print each item on the receipt
const messageEl = document.createElement('div');
variants.forEach((variant, i) => {
messageEl.innerHTML += `${variant.quantity} × ${variant.name}<br>`;
});
messageEl.style.textAlign = 'left';
// Check if they left a custom message
let customMessageEl = document.createElement('div');
const customMessage = data["fw.statmessageus"];
if (customMessage)
{
const txt = document.createElement("textarea");
txt.innerHTML = customMessage;
customMessageEl.innerHTML += `<br><i>${txt.value}</i>`;
}
// Add a cute thank you message because you're uwu like that
const thankYouEl = document.createElement('div');
thankYouEl.innerHTML += `<br><b>Thank you for your purchase!</b>`;
contentEl.appendChild(messageEl);
contentEl.appendChild(customMessageEl);
contentEl.appendChild(thankYouEl);
}
break;
case ('FourthwallSubscriptionPurchased'):
{
const avatarURL = await GetAvatar(data["fw.nickname"], 'twitch');
if (IsValidUrl(avatarURL))
avatarEl.src = avatarURL;
else
avatarEl.style.display = 'none'
titleEl.innerText = `New Member`;
subtitleEl.innerHTML = `${data["fw.nickname"]}`;
const messageEl = document.createElement('div');
messageEl.innerHTML = `Thanks for joining at the <b>${FormatCurrency(data["fw.amount"], data["fw.currency"])}</b> tier!`;
contentEl.appendChild(messageEl);
}
break;
// Custom Code Events
case ('CustomCodeEvent'):
{
switch (data.triggerCustomCodeEventName) {
case ('kickIncomingRaid'):
{
avatarEl.src = ConvertWEBPToPNG(await GetAvatar(data.user, 'kick'));
const messageEl = document.createElement('div');
messageEl.innerHTML = `<b>${data.user}</b><br>is hosting with a party of<br><b>${data.viewers} viewers!</b>`;
contentEl.appendChild(messageEl);
// Set the platform icon
SetPlatformIcon(iconEl, 'kick');
}
break;
}
}
break;
// Don't print any event not excplicitly listed above
default:
return;
}
// Set the timestamp
const { DateTime } = luxon;
const now = DateTime.local();
const formatted = now.toFormat("cccc, LLLL d',' yyyy HH:mm:ss");
// Add ordinal suffix manually
function addOrdinal(n) {
if (n >= 11 && n <= 13) return 'th';
switch (n % 10) {
case 1: return 'st';
case 2: return 'nd';
case 3: return 'rd';
default: return 'th';
}
}
const day = now.day;
const ordinal = addOrdinal(day);
const fullFormatted = now.toFormat(`cccc, LLLL '${day}${ordinal}' yyyy HH:mm:ss`);
dateEl.textContent = fullFormatted;
// Send it to the print routine!
const receiptHTML = await GetRenderedHTML(instance);
console.log(receiptHTML);
client.doAction({ name: 'Printer Bot | Print Routine' }, {
receiptHTML: receiptHTML,
isTest: data.isTest
});
}
//////////////////////
// HELPER FUNCTIONS //
//////////////////////
async function GetAvatar(username, platform) {
// First, check if the username is hashed already
if (avatarMap.has(`${username}-${platform}`)) {
console.debug(`Avatar found for ${username} (${platform}). Retrieving from hash map.`)
return avatarMap.get(`${username}-${platform}`);
}
// If code reaches this point, the username hasn't been hashed, so retrieve avatar
switch (platform) {
case 'twitch':
{
console.debug(`No avatar found for ${username} (${platform}). Retrieving from Decapi.`)
let response = await fetch('https://decapi.me/twitch/avatar/' + username);
let data = await response.text();
avatarMap.set(`${username}-${platform}`, data);
return data;
}
case 'kick':
{
console.debug(`No avatar found for ${username} (${platform}). Retrieving from Kick.`)
try {
let response = await fetch('https://kick.com/api/v2/channels/' + username);
console.log('https://kick.com/api/v2/channels/' + username)
let data = await response.json();
let avatarURL = data.user.profile_pic;
if (!avatarURL)
avatarURL = 'https://kick.com/img/default-profile-pictures/default2.jpeg';
avatarMap.set(`${username}-${platform}`, avatarURL);
return avatarURL;
}
catch (error) {
console.debug(error);
return 'https://kick.com/img/default-profile-pictures/default2.jpeg';
}
}
}
}
async function GetRenderedHTML(fragment) {
if (!(fragment instanceof DocumentFragment)) {
throw new Error('Argument must be a DocumentFragment');
}
// Filter out comment nodes from fragment content
const nodes = Array.from(fragment.childNodes).filter(
node => node.nodeType !== Node.COMMENT_NODE
);
const bodyContent = nodes
.map(node => node.outerHTML || node.textContent)
.join('');
// Get inline <style> contents
const inlineStyles = Array.from(document.querySelectorAll('style'))
.map(style => style.textContent)
.join('\n');
// Get all external stylesheet URLs
const linkHrefs = Array.from(document.querySelectorAll('link[rel="stylesheet"]'))
.map(link => link.href);
// Fetch external CSS contents
const externalCSSContents = await Promise.all(
linkHrefs.map(async href => {
try {
const res = await fetch(href);
if (!res.ok) throw new Error(`Failed to load CSS from ${href}`);
return await res.text();
} catch {
console.warn(`Could not fetch CSS from ${href}`);
return '';
}
})
);
// Combine all CSS into one string
const combinedCSS = inlineStyles + '\n' + externalCSSContents.join('\n');
// Build the full standalone HTML string
const fullHTML = `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Exported Page</title>
<style>${combinedCSS}</style>
</head>
<body>
${bodyContent}
</body>
</html>`.trim();
return fullHTML;
}
function ConvertWEBPToPNG(URL) {
return `https://images.weserv.nl/?url=${URL}&output=png`;
}
function FormatCurrency(amount, currency) {
const isISOCode = /^[A-Z]{3}$/.test(currency);
if (isISOCode) {
try {
return new Intl.NumberFormat(undefined, {
style: 'currency',
currency: currency,
currencyDisplay: 'symbol',
}).format(amount);
} catch {
return `${amount.toFixed(2)} ${currency}`;
}
}
// Handle some common symbols that go before the number
const symbolsBefore = ['$', '€', '£', '¥', '₹'];
if (symbolsBefore.includes(currency)) {
return `${currency}${amount.toFixed(2)}`;
}
// Otherwise default to appending after
return `${amount.toFixed(2)} ${currency}`;
}
function IsValidUrl(string) {
try {
new URL(string);
return true;
} catch {
return false;
}
}
function EscapeRegExp(string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
}
function SetPlatformIcon(el, platform) {
// Set the platform icon
let baseURL = window.location.href;
baseURL = baseURL.replace(/index\.html$/i, '');
el.src = `${baseURL}/icons/platforms/${platform}.png`;
}
///////////////////////////////////
// STREAMER.BOT WEBSOCKET STATUS //
///////////////////////////////////
function SetConnectionState(isConnected) {
if (isConnected) {
document.getElementById('ip').disabled = true;
document.getElementById('port').disabled = true;
document.getElementById('connect-button').style.backgroundColor = '#e43b3b';
document.getElementById('connect-button').innerText = 'Disconnect';
localStorage.setItem('sbServerAddress', document.getElementById('ip').value);
localStorage.setItem('sbServerPort', document.getElementById('port').value);
}
else {
document.getElementById('ip').disabled = false;
document.getElementById('port').disabled = false;
document.getElementById('connect-button').style.backgroundColor = '#3be477';
document.getElementById('connect-button').innerText = 'Connect';
}
}
function Connect() {
if (document.getElementById('ip').disabled)
client.disconnect();
else
{
client.options.host = document.getElementById('ip').value;
client.options.port = document.getElementById('port').value;
client.connect();
}
}
+127
View File
@@ -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;
}