Hide bundles on Humble Bundle's bundles page. Hidden bundles persist via localStorage.
// ==UserScript==
// @name HumbleBundle Bundle Hider
// @namespace https://www.humblebundle.com/
// @version 1.1.0
// @description Hide bundles on Humble Bundle's bundles page. Hidden bundles persist via localStorage.
// @author mcbyte
// @license MIT
// @match https://www.humblebundle.com/bundles*
// @grant none
// @run-at document-idle
// ==/UserScript==
(function () {
'use strict';
const STORAGE_KEY = 'hb_hidden_bundles';
function getHidden() {
try { return new Set(JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]')); }
catch { return new Set(); }
}
function saveHidden(set) {
localStorage.setItem(STORAGE_KEY, JSON.stringify([...set]));
}
function getBundleKey(anchor) {
try { return (anchor.getAttribute('href') || '').split('?')[0]; }
catch { return null; }
}
// ── Styles ────────────────────────────────────────────────────────────────
const style = document.createElement('style');
style.textContent = `
.hb-hide-btn {
position: absolute; top: 6px; right: 6px; z-index: 999;
width: 28px; height: 28px; border-radius: 50%;
background: rgba(0,0,0,.55); border: none; cursor: pointer;
display: flex; align-items: center; justify-content: center;
opacity: 0; transition: opacity .2s ease, background .15s ease;
padding: 0; line-height: 1;
}
.tile-holder:hover .hb-hide-btn,
.hb-hide-btn:focus-visible { opacity: 1; }
.hb-hide-btn:hover { background: rgba(220,30,30,.75); }
.hb-hide-btn svg { pointer-events: none; }
.tile-holder.hb-hidden { display: none !important; }
#hb-show-hidden-btn {
position: absolute; top: 8px; right: 16px; z-index: 1000;
display: flex; align-items: center; gap: 6px;
padding: 5px 11px; background: rgba(0,0,0,.6); color: #fff;
border: 1px solid rgba(255,255,255,.2); border-radius: 20px;
cursor: pointer; font-size: 13px; font-family: inherit;
transition: background .15s ease;
}
#hb-show-hidden-btn:hover { background: rgba(0,0,0,.8); }
#hb-show-hidden-btn .hb-count {
background: #e87919; border-radius: 10px;
padding: 0 6px; font-size: 11px; font-weight: 700; line-height: 18px;
}
#hb-show-hidden-btn.hb-showing { background: rgba(232,121,25,.8); }
`;
document.head.appendChild(style);
const ICON_EYE_OFF = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"
fill="none" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94"/>
<path d="M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19"/>
<line x1="1" y1="1" x2="23" y2="23"/>
</svg>`;
const ICON_EYE = `<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24"
fill="none" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/>
<circle cx="12" cy="12" r="3"/>
</svg>`;
let hidden = getHidden();
let showingHidden = false;
let debounceTimer = null;
// ── MutationObserver — ONLY reacts to new tile nodes, debounced ───────────
function onMutation(mutations) {
let hasNewTiles = false;
for (const m of mutations) {
if (!m.addedNodes.length) continue;
for (const node of m.addedNodes) {
if (!(node instanceof Element)) continue;
if (node.classList.contains('tile-holder') || node.querySelector('.tile-holder')) {
hasNewTiles = true;
break;
}
}
if (hasNewTiles) break;
}
if (!hasNewTiles) return;
clearTimeout(debounceTimer);
debounceTimer = setTimeout(decorateAllTiles, 300);
}
function applyHiddenState(tileHolder, key) {
if (hidden.has(key)) {
if (showingHidden) {
tileHolder.classList.remove('hb-hidden');
tileHolder.style.opacity = '0.35';
tileHolder.style.filter = 'grayscale(80%)';
} else {
tileHolder.classList.add('hb-hidden');
tileHolder.style.opacity = '';
tileHolder.style.filter = '';
}
} else {
tileHolder.classList.remove('hb-hidden');
tileHolder.style.opacity = '';
tileHolder.style.filter = '';
}
}
function decorateTile(tileHolder) {
if (tileHolder.dataset.hbDecorated) return;
tileHolder.dataset.hbDecorated = '1';
const anchor = tileHolder.querySelector('a.full-tile-view');
if (!anchor) return;
const key = getBundleKey(anchor);
if (!key) return;
if (getComputedStyle(tileHolder).position === 'static')
tileHolder.style.position = 'relative';
const btn = document.createElement('button');
btn.className = 'hb-hide-btn';
btn.setAttribute('aria-label', 'Hide this bundle');
btn.title = 'Hide this bundle';
btn.innerHTML = ICON_EYE_OFF;
btn.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
hidden.has(key) ? hidden.delete(key) : hidden.add(key);
saveHidden(hidden);
applyHiddenState(tileHolder, key);
updateToggleButton();
});
tileHolder.appendChild(btn);
applyHiddenState(tileHolder, key);
}
function decorateAllTiles() {
document.querySelectorAll('.tile-holder.js-tile-holder').forEach(decorateTile);
}
function updateToggleButton() {
const btn = document.getElementById('hb-show-hidden-btn');
if (!btn) return;
const count = hidden.size;
btn.querySelector('.hb-count').textContent = count;
btn.style.display = count === 0 ? 'none' : 'flex';
btn.classList.toggle('hb-showing', showingHidden);
btn.title = showingHidden ? 'Collapse hidden bundles' : `Show ${count} hidden bundle(s)`;
}
function createToggleButton() {
const mosaicSection = document.querySelector('.landing-mosaic-section');
if (!mosaicSection || document.getElementById('hb-show-hidden-btn')) return;
if (getComputedStyle(mosaicSection).position === 'static')
mosaicSection.style.position = 'relative';
const btn = document.createElement('button');
btn.id = 'hb-show-hidden-btn';
btn.innerHTML = `${ICON_EYE} <span>Hidden</span> <span class="hb-count">${hidden.size}</span>`;
btn.title = 'Show hidden bundles';
btn.style.display = hidden.size === 0 ? 'none' : 'flex';
btn.addEventListener('click', () => {
showingHidden = !showingHidden;
document.querySelectorAll('.tile-holder.js-tile-holder[data-hb-decorated]').forEach(th => {
const a = th.querySelector('a.full-tile-view');
if (!a) return;
const k = getBundleKey(a);
if (k) applyHiddenState(th, k);
});
updateToggleButton();
});
mosaicSection.prepend(btn);
}
function init() {
createToggleButton();
decorateAllTiles();
const observer = new MutationObserver(onMutation);
observer.observe(document.body, { childList: true, subtree: true });
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();