Hide habs from habr
// ==UserScript==
// @name Habr Visor
// @match https://habr.com/*
// @grant GM_addStyle
// @namespace http://tampermonkey.net/
// @version 1.0.1
// @description Hide habs from habr
// @author You
// @icon https://www.google.com/s2/favicons?sz=64&domain=tampermonkey.net
// @license MIT
// @run-at document-end
// ==/UserScript==
const KEY = 'habr-visor-data';
const STYLE_ID = 'v-visor-styles';
const ICON = `<svg viewBox="0 0 512 512" style="width:20px;height:20px;color:var(--color-text-second,#999);pointer-events:none"><circle cx="256" cy="256" r="64" fill="currentColor"/><path d="M490.84 238.6c-26.46-40.92-60.79-75.68-99.27-100.53C349 110.55 302 96 255.66 96c-42.52 0-84.33 12.15-124.27 36.11c-40.73 24.43-77.63 60.12-109.68 106.07a31.92 31.92 0 0 0-.64 35.54c26.41 41.33 60.4 76.14 98.28 100.65C162 402 207.9 416 255.66 416c46.71 0 93.81-14.43 136.2-41.72c38.46-24.77 72.72-59.66 99.08-100.92a32.2 32.2 0 0 0-.1-34.76zM256 352a96 96 0 1 1 96-96a96.11 96.11 0 0 1-96 96z" fill="currentColor"/></svg>`;
GM_addStyle(`
#vBtn { cursor: pointer; padding: 8px; display: flex; align-items: center; border-radius: 4px; position: relative; margin-right: 8px; align-self: center; z-index: 1000; }
#vBtn:hover { background: var(--color-background-field, rgba(128,128,128,0.1)); }
.v-pop {
position: absolute; top: 48px; right: 0; width: 320px; padding: 20px; border-radius: 12px;
box-shadow: 0 12px 40px rgba(0,0,0,0.5); display: none; flex-direction: column;
background: var(--color-background-card, #fff); border: 1px solid var(--color-border-elements, #ddd);
color: var(--color-text-main, #333); z-index: 10001; font-size: 16px;
}
@media (prefers-color-scheme: dark) {
.v-pop { background: #1c1c1d; border-color: #333; color: #eee; }
.v-in { background: #2d2d2e!important; border-color: #444!important; color: #fff!important; }
}
.v-tags { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 12px; max-height: 150px; overflow-y: auto; }
.v-tag { background: var(--color-primary-main, #5d83b3); color: #fff!important; padding: 4px 10px; border-radius: 6px; font-size: 14px; display: flex; align-items: center; gap: 8px; line-height: 1.2; }
.v-tag b { cursor: pointer; font-size: 18px; line-height: 1; }
.v-in { width: 100%; padding: 12px; border: 1px solid var(--color-border-elements); border-radius: 6px; outline: none; margin-bottom: 14px; box-sizing: border-box; font-size: 16px; }
.v-footer { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; font-weight: bold; }
.v-clear { text-decoration: underline; cursor: pointer; opacity: 0.6; font-size: 14px; font-weight: normal; }
`);
let state = { tags: [], show: false };
let lastCss = "";
try {
const s = localStorage.getItem(KEY);
if (s) state = JSON.parse(s);
} catch (e) {
console.error("Visor Error:", e);
}
const updatePageStyles = () => {
const activeTags = state.tags.map(t => t.toLowerCase());
const blockedIds = [];
const articles = document.querySelectorAll('article');
for (const art of articles) {
const hubs = Array.from(art.querySelectorAll('.tm-publication-hub__link span, .tm-article-snippet__hubs-item'))
.map(s => s.innerText.trim().toLowerCase());
const shouldBlock = activeTags.some(t => hubs.some(h => h.includes(t)));
if (shouldBlock) {
const id = art.id || art.getAttribute('data-aid');
if (id) blockedIds.push(`[id="${id}"]`);
}
};
const currentCount = blockedIds.length;
const cssContent = currentCount > 0
? (state.show
? `${blockedIds.join(', ')} { opacity: 0.45 !important; filter: grayscale(1) !important; }`
: `${blockedIds.join(', ')} { display: none !important; }`)
: "";
if (cssContent !== lastCss) {
let styleTag = document.getElementById(STYLE_ID);
if (!styleTag) {
styleTag = document.createElement('style');
styleTag.id = STYLE_ID;
document.head.appendChild(styleTag);
}
styleTag.textContent = cssContent;
lastCss = cssContent;
}
const counter = document.getElementById('vCount');
if (counter) counter.innerText = currentCount;
};
const save = () => {
localStorage.setItem(KEY, JSON.stringify(state));
renderTags();
updatePageStyles();
};
const renderTags = () => {
const cont = document.querySelector('.v-tags');
if (!cont) return;
cont.innerHTML = state.tags.map((t, i) => `<span class="v-tag">${t}<b data-idx="${i}">×</b></span>`).join('');
cont.querySelectorAll('b').forEach(b => {
b.onclick = (e) => {
e.stopPropagation();
state.tags.splice(parseInt(b.dataset.idx, 10), 1);
save();
};
});
};
const inject = () => {
const header = document.querySelector('.tm-header__container');
if (!header || document.getElementById('vBtn')) return;
const btn = document.createElement('div');
btn.id = 'vBtn';
btn.innerHTML = `${ICON}<div class="v-pop" id="vPop">
<div class="v-footer"><span>Фильтры хабов</span><span class="v-clear" id="vClear">Очистить всё</span></div>
<div class="v-tags"></div>
<input class="v-in" placeholder="Название хаба + Enter...">
<label style="display:flex;align-items:center;gap:10px;cursor:pointer;user-select:none">
<input type="checkbox" id="vS" ${state.show ? 'checked' : ''}> Показать скрытое (<span id="vCount">0</span>)
</label>
</div>`;
header.appendChild(btn);
btn.onclick = (e) => {
if (e.target.closest('.v-pop')) return;
const p = document.getElementById('vPop');
p.style.display = p.style.display === 'flex' ? 'none' : 'flex';
};
btn.querySelector('#vClear').onclick = (e) => { e.stopPropagation(); state.tags = []; save(); };
btn.querySelector('#vS').onchange = (e) => { state.show = e.target.checked; save(); };
const input = btn.querySelector('.v-in');
input.onkeydown = (e) => {
if (e.key === 'Enter' && input.value.trim()) {
const val = input.value.trim();
if (!state.tags.includes(val)) state.tags.push(val);
input.value = '';
save();
}
};
renderTags();
updatePageStyles();
};
let timer;
const observer = new MutationObserver(() => {
clearTimeout(timer);
timer = setTimeout(() => {
inject();
updatePageStyles();
}, 300);
});
observer.observe(document.body, { childList: true, subtree: true });
inject();
updatePageStyles();