// ==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 = `
`;
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();
});
})();