// ==UserScript== // @name Twitch Channel Points QOL // @description Hides & Renames Twitch Channel Point Rewards // @author Lone Destroyer // @license MIT // @match https://www.twitch.tv/* // @icon https://static.twitchcdn.net/assets/favicon-32-e29e246c157142c94346.png // @version 2.0.3 // @namespace https://github.com/LoneDestroyer // @downloadURL https://raw.githubusercontent.com/LoneDestroyer/Twitch-Channel-Points-QOL/main/Twitch-Channel-Points-QOL.user.js // @updateURL https://raw.githubusercontent.com/LoneDestroyer/Twitch-Channel-Points-QOL/main/Twitch-Channel-Points-QOL.user.js // ==/UserScript== (function() { 'use strict'; // --- Selectors --- const rewardsPanelSelector = '#channel-points-reward-center-body'; // Rewards Panel const rewardsBodySelector = '.reward-center__content'; // Rewards Center Content const rewardSelector = 'div.cXZCZH' //'.reward-list-item'; // Rewards Item const rewardTextSelector = 'div.cXZCZH > div:nth-child(1) > div:nth-child(2) > p' //'.reward-list-item > div:nth-child(1) > div:nth-child(2) > p'; // Reward Text const rewardsPanelFooterSelector = '.reward-center__content > div:nth-child(3) > div:nth-child(1) > div:nth-child(1) > div:nth-child(1)'; // Footer for Restore Rewards Button const rewardsDescriptionSelector = '.reward-center-body > div:nth-child(1) > div:nth-child(1) > p:nth-child(1)'; // Rewards Description // --- Power-Ups Selector --- function getPowerUpsTitleElement() { return Array.from(document.querySelectorAll('p')).find( powerUpsTitleEl => powerUpsTitleEl.textContent.trim().toLowerCase() === 'power-ups' ); } // --- Icon mappings --- const iconMappings = { Steam: 'https://store.steampowered.com/favicon.ico', GOG: 'https://www.gog.com/favicon.ico', EA: 'https://upload.wikimedia.org/wikipedia/commons/0/0d/Electronic-Arts-Logo.svg', Microsoft: 'https://www.microsoft.com/favicon.ico', 'Microsoft Store': 'https://www.microsoft.com/favicon.ico', 'Windows 10': 'https://www.microsoft.com/favicon.ico', 'Windows 11': 'https://www.microsoft.com/favicon.ico', Windows: 'https://www.microsoft.com/favicon.ico', Xbox: 'https://www.xbox.com/favicon.ico', EXPIRED: 'https://upload.wikimedia.org/wikipedia/commons/e/e5/Fluent_Emoji_flat_1f6ab.svg', ENDED: 'https://upload.wikimedia.org/wikipedia/commons/e/e5/Fluent_Emoji_flat_1f6ab.svg', Entry: 'https://cdn0.iconfinder.com/data/icons/travel-258/64/ticket_entry_pass_entrance_coupon_-512.png', }; // Get removed rewards from localStorage function getRemovedRewards() { return JSON.parse(localStorage.getItem('removedRewards')) || []; } // Save removed rewards to localStorage function saveRemovedReward(rewardText) { const removedRewards = getRemovedRewards(); if (!removedRewards.includes(rewardText)) { removedRewards.push(rewardText); localStorage.setItem('removedRewards', JSON.stringify(removedRewards)); } } // Remove a reward from localStorage function removeRewardFromStorage(rewardText) { const removedRewards = getRemovedRewards(); const updatedRewards = removedRewards.filter(text => text !== rewardText); localStorage.setItem('removedRewards', JSON.stringify(updatedRewards)); } // Helper to set display for rewards by name (using original text) function setRewardsDisplay(rewardNames, displayValue) { const rewardElements = document.querySelectorAll(rewardSelector); rewardElements.forEach(rewardElement => { // Use original text if present, otherwise use current text (modified with icon) const rewardTextEl = rewardElement.querySelector(rewardTextSelector); let originalText = rewardElement.dataset.originalReward || (rewardTextEl ? rewardTextEl.textContent.trim() : ""); if (rewardNames.includes(originalText)) { rewardElement.style.display = displayValue; } }); } // Hide rewards based on localStorage function hideRemovedRewards() { const removedRewards = getRemovedRewards(); setRewardsDisplay(removedRewards, 'none'); // Hide Power-Ups if needed if (removedRewards.includes('Twitch Power-Ups')) { togglePowerUpsVisibility(true); } } // Restore a specific reward function restoreReward(rewardText) { removeRewardFromStorage(rewardText); if (rewardText === 'Twitch Power-Ups') { togglePowerUpsVisibility(false); // Show Power-Ups } else { setRewardsDisplay([rewardText], ''); } } // Toggle visibility of the restore container function toggleRestoreContainer() { let restoreContainerEl = document.querySelector('#restore-container'); if (restoreContainerEl) { restoreContainerEl.style.display = restoreContainerEl.style.display === 'none' ? 'block' : 'none'; } } // Add a "Restore Rewards" button to channel points footer function addRestoreRewardsButton() { const rewardsPanelFooterEl = document.querySelector(rewardsPanelFooterSelector); if (rewardsPanelFooterEl && !document.querySelector('#restore-rewards-button')) { const restoreRewardsButton = document.createElement('button'); restoreRewardsButton.id = 'restore-rewards-button'; restoreRewardsButton.className = 'ScCoreButton-sc-ocjdkq-0 yezmM'; // Twitch button classes restoreRewardsButton.style = `box-shadow: 0 0 0 2px color-mix(in srgb, var(--color-green-13) 30%, transparent), 0 1px 4px rgba(0,0,0,0.12); margin-inline: 8px 8px !important`; // Button Label const restoreLabelInner = document.createElement('div'); restoreLabelInner.setAttribute('data-a-target', 'tw-core-button-label-text'); restoreLabelInner.className = 'Layout-sc-1xcs6mc-0 bLZXTb'; // Clock Icon const restoreIconDiv = document.createElement('div'); restoreIconDiv.className = 'Layout-sc-1xcs6mc-0 eynyeD'; restoreIconDiv.innerHTML = `
`; // FA Clock icon (green) // Button text const restoreTextDiv = document.createElement('div'); restoreTextDiv.className = 'Layout-sc-1xcs6mc-0 fHdBNk'; restoreTextDiv.textContent = 'Restore'; restoreIconDiv.appendChild(restoreTextDiv); const restoreLabelOuter = document.createElement('div'); restoreLabelOuter.className = 'ScCoreButtonLabel-sc-s7h2b7-0 kaIUar'; restoreLabelInner.appendChild(restoreIconDiv); restoreLabelOuter.appendChild(restoreLabelInner); restoreRewardsButton.appendChild(restoreLabelOuter); restoreRewardsButton.title = 'Restore Hidden Rewards'; restoreRewardsButton.addEventListener('click', toggleRestoreContainer); rewardsPanelFooterEl.parentElement.appendChild(restoreRewardsButton); } } // Function to add a restore button to each hidden reward function addRestoreButtons() { const removedRewards = getRemovedRewards(); // Create a container for restore buttons if it doesn't exist let restoreContainerEl = document.querySelector('#restore-container'); if (!restoreContainerEl) { // Create the outer container const restoreContainerOuter = document.createElement('div'); restoreContainerOuter.id = 'restore-container'; restoreContainerOuter.style = ` position: absolute; inset: auto auto 100% 0px; margin-right: 10px; width: 320px; height: 400px; background-color: var(--color-background-base) !important; padding: 10px; box-sizing: border-box; box-shadow: var(--shadow-elevation-2) !important; display: none; border-radius: var(--border-radius-large) !important; `; // Add a header to the outer container const restoreContainerHeader = document.createElement('div'); restoreContainerHeader.textContent = 'Restore Hidden Rewards'; restoreContainerHeader.style = ` font-size: var(--font-size-3) !important; font-weight: var(--font-weight-normal) !important; font-family: var(--font-display); text-align: left !important; display: flex; align-items: center; color: #efeffi; padding: 5px 0; border-radius: 5px 5px 0 0;} line-height: var(--line-height-heading) !important; letter-spacing: -0.18px !important; `; // Add a close button to the header const closeButton = document.createElement('button'); closeButton.className = 'ScCoreButton-sc-ocjdkq-0 iPkwTD ScButtonIcon-sc-9yap0r-0 dcNXJO'; closeButton.setAttribute('aria-label', 'Close'); closeButton.title = 'Close Restore Rewards Panel'; const closeIconDiv = document.createElement('div'); closeIconDiv.className = 'ButtonIconFigure-sc-1emm8lf-0 lnTwMD'; const closeIcon = document.createElement('div'); closeIcon.className = 'ScSvgWrapper-sc-wkgzod-0 kccyMt tw-svg'; closeIcon.innerHTML = ` `; // Twitch close icon closeIconDiv.appendChild(closeIcon); closeButton.appendChild(closeIconDiv); const closeButtonOuter = document.createElement('div'); closeButtonOuter.className = 'Layout-sc-1xcs6mc-0 iieUvQ'; closeButtonOuter.style = `padding-left: 75px;`; closeButtonOuter.appendChild(closeButton); // On click closes the restore container closeButton.addEventListener('click', () => { restoreContainerOuter.style.display = 'none'; // Hide }); restoreContainerHeader.appendChild(closeButtonOuter); restoreContainerOuter.appendChild(restoreContainerHeader); // Create the scrollable container for buttons const restoreButtonsContainer = document.createElement('div'); restoreButtonsContainer.id = 'restore-buttons-container'; restoreButtonsContainer.style = ` max-height: calc(100% - 50px); max-width: calc(100%); overflow: auto; scrollbar-color: grey transparent; scrollbar-width: thin; display: flex; flex-direction: column; border-radius: 0 0 5px 5px; padding: 2px; `; restoreContainerOuter.appendChild(restoreButtonsContainer); // Append the outer container to the Rewards Body const rewardsBodyEl = document.querySelector(rewardsBodySelector); if (rewardsBodyEl) { rewardsBodyEl.parentElement.insertBefore(restoreContainerOuter, rewardsBodyEl); } else { document.body.appendChild(restoreContainerOuter); // Fallback if target div is not found } } // Adds restore buttons for each removed reward removedRewards.forEach(rewardText => { const restoreButtonsContainer = document.querySelector('#restore-buttons-container'); if (!restoreButtonsContainer.querySelector(`.restore-reward-button[data-reward="${rewardText}"]`)) { const restoreButton = document.createElement('button'); restoreButton.className = 'ScCoreButton-sc-ocjdkq-0 yezmM restore-reward-button'; restoreButton.style = ` box-shadow: 0 0 0 2px color-mix(in srgb,${rewardText === 'Twitch Power-Ups'?'var(--color-blue-9)':'var(--color-green-13)'} 70%, transparent),0 1px 4px rgba(0, 0, 0, 0.12); margin-left: 10px; justify-content: normal !important; margin: 4px 0; min-height: 32px; min-width: 0; border: none; display: flex; `; restoreButton.title = `Restore: "${rewardText}"`; restoreButton.dataset.reward = rewardText; // Build Twitch-style label structure const restoreButtonsOuter = document.createElement('div'); restoreButtonsOuter.className = 'ScCoreButtonLabel-sc-s7h2b7-0 kaIUar'; const restoreButtonsInner = document.createElement('div'); restoreButtonsInner.setAttribute('data-a-target', 'tw-core-button-label-text'); restoreButtonsInner.className = 'Layout-sc-1xcs6mc-0 bLZXTb'; const iconAndText = document.createElement('div'); iconAndText.className = 'Layout-sc-1xcs6mc-0 eynyeD'; // Add icon and text const { iconHtml, displayText } = getRewardIconAndText(rewardText, iconMappings); const restoreButtonsIcon = document.createElement('span'); restoreButtonsIcon.innerHTML = iconHtml; // Text const restoreButtonsText = document.createElement('div'); restoreButtonsText.className = 'Layout-sc-1xcs6mc-0 fHdBNk'; restoreButtonsText.textContent = displayText; // Assemble iconAndText.appendChild(restoreButtonsIcon); iconAndText.appendChild(restoreButtonsText); restoreButtonsInner.appendChild(iconAndText); restoreButtonsOuter.appendChild(restoreButtonsInner); restoreButton.appendChild(restoreButtonsOuter); restoreButton.addEventListener('click', () => { restoreReward(rewardText); restoreButton.remove(); // Remove the button after restoring }); restoreButtonsContainer.insertBefore(restoreButton, restoreButtonsContainer.firstChild); } }); } // Helper for remove buttons function styleRemoveButton(btn, color = 'rgba(0, 0, 0, 0.6)', hoverColor = 'rgba(0, 0, 0, 0.8)', extraStyle = {}) { btn.innerHTML = ` `; btn.style = ` position: absolute; top: 5px; right: 5px; width: 20px; height: 20px; background-color: rgba(0, 0, 0, 0.6); color: #fff; border-radius: 50%; display: flex; align-items: center; justify-content: center; z-index: 10; font-size: 12px; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); transition: background-color 0.3s, transform 0.2s; `; // Allow extra style overrides - provides flexibility for (const [k, v] of Object.entries(extraStyle)) { btn.style[k] = v; } btn.addEventListener('mouseover', () => { btn.style.backgroundColor = hoverColor; btn.style.transform = 'scale(1.1)'; btn.title = 'Hide'; }); btn.addEventListener('mouseout', () => { btn.style.backgroundColor = color; btn.style.transform = 'scale(1)'; btn.title = ''; }); } // Rename rewards based on the text content & Ability to remove the Power-Ups - Adds eye button to hide function updateRewards() { const rewardElements = document.querySelectorAll(rewardSelector); rewardElements.forEach(rewardElement => { const rewardTextEl = rewardElement.querySelector(rewardTextSelector); if (rewardTextEl) { let rewardName = rewardTextEl.textContent.trim(); // Store the original text as a data attribute if not already set if (!rewardElement.dataset.originalReward) { rewardElement.dataset.originalReward = rewardName; } // Add a "Remove" button if (!rewardElement.querySelector('.remove-reward-button')) { const removeRewardButton = document.createElement('button'); removeRewardButton.className = 'remove-reward-button'; styleRemoveButton(removeRewardButton, 'rgba(0,0,0,0.6)', 'rgba(0,0,0,0.8)', { position: 'absolute', top: '5px', right: '5px' }); removeRewardButton.addEventListener('click', () => { rewardElement.style.display = 'none'; // Hide the specific reward saveRemovedReward(rewardElement.dataset.originalReward); // Save original to localStorage addRestoreButtons(); // Update the restore list }); rewardElement.style.position = 'relative'; rewardElement.appendChild(removeRewardButton); } // Rename rewards based on mappings const { iconHtml, displayText } = getRewardIconAndText(rewardName, iconMappings); if (iconHtml) { rewardTextEl.innerHTML = `${iconHtml} ${displayText}`; } } }); } // Hide / show Power-Ups function togglePowerUpsVisibility(hide) { const powerUpsEl = document.querySelector('#power-ups-style'); if (hide) { if (!powerUpsEl) { const powerUpStyle = document.createElement('style'); powerUpStyle.id = 'power-ups-style'; powerUpStyle.textContent = ` .rewards-list > div:first-of-type, .rewards-list [class*="bitsRewardListItem"] { display: none !important; } .rewards-list > div { padding-top: 0 !important; } `; document.head.appendChild(powerUpStyle); } saveRemovedReward('Twitch Power-Ups'); // Add Power-Ups to removedRewards } else { if (powerUpsEl) { powerUpsEl.remove(); } removeRewardFromStorage('Twitch Power-Ups'); // Remove Power-Ups from removedRewards } } // Ability to remove the Power-Ups - Adds eye button to hide function removePowerupButton() { const powerUpsTitleEl = getPowerUpsTitleElement(); if (powerUpsTitleEl && !powerUpsTitleEl.querySelector('.remove-powerup-button')) { const removePowerupButton = document.createElement('button'); removePowerupButton.className = 'remove-powerup-button'; styleRemoveButton(removePowerupButton,'rgba(64, 64, 64, 0.6)','rgba(64, 64, 64, 0.8)', { marginLeft: '8px', position: 'static', verticalAlign: 'middle', display: 'inline-flex' } ); removePowerupButton.addEventListener('click', () => { togglePowerUpsVisibility(true); // Hide rewards }); powerUpsTitleEl.appendChild(removePowerupButton); } } // Helper function for reward icon and display text function getRewardIconAndText(rewardText, iconMappings) { for (const [key, iconUrl] of Object.entries(iconMappings)) { if (rewardText.startsWith(key)) { const displayText = rewardText.replace( new RegExp(`^${key}:?(?:\\s*(?:Key Giveaway:?|APP Key:?|Key:?))?\\s*`, 'i'), '' ).trim(); const iconHtml = `${key}`; return { iconHtml, displayText }; } } return { iconHtml: '', displayText: rewardText }; } // Converts URLs in the rewardsDescription to clickable links function linkifyDescription() { const rewardsDescripEl = document.querySelector(rewardsDescriptionSelector); if (rewardsDescripEl && rewardsDescripEl.childNodes.length === 1 && rewardsDescripEl.childNodes[0].nodeType === Node.TEXT_NODE) { const rewardsDectipText = rewardsDescripEl.textContent; const txt2UrlRegex = /(https?:\/\/[^\s]+)/g; if (txt2UrlRegex.test(rewardsDectipText)) { const txt2UrlHtml = rewardsDectipText.replace(txt2UrlRegex, url => `${url}`); rewardsDescripEl.innerHTML = txt2UrlHtml; } } } // Function to observe the rewards panel function observeRewardsPanel() { const observer = new MutationObserver(() => { if (document.querySelector(rewardsPanelSelector)) { hideRemovedRewards(); // Hide rewards based on localStorage updateRewards(); // Hide and rename rewards removePowerupButton(); // Add button to hide powerups addRestoreButtons(); // Add restore buttons for hidden rewards addRestoreRewardsButton(); // Add the "Restore Rewards" button next to the target button linkifyDescription(); // Convert URLs in the rewards description to clickable links } }); observer.observe(document.body, { childList: true, subtree: true }); } // Wait until the page has fully loaded window.addEventListener('load', () => { observeRewardsPanel(); }); })();