您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Hide posts and authors in lists/search on kemono.cr and coomer.st. Backup/import your Blacklist via file.
当前为
// ==UserScript== // @name Kemono/Coomer Blacklist with backup // @version 1.5 // @description Hide posts and authors in lists/search on kemono.cr and coomer.st. Backup/import your Blacklist via file. // @author glauthentica // @match https://kemono.cr/* // @match https://*.kemono.cr/* // @match https://coomer.st/* // @match https://*.coomer.st/* // @run-at document-idle // @grant GM.getValue // @grant GM.setValue // @grant GM.download // @grant GM_download // @license MIT // @homepageURL https://boosty.to/glauthentica // @contributionURL https://boosty.to/glauthentica // @namespace https://tampermonkey.net/ // ==/UserScript== (function() { 'use strict'; const getCurrentService = () => { const host = location.hostname; if (host.includes('kemono')) return 'kemono'; if (host.includes('coomer')) return 'coomer'; return 'unknown'; }; const CURRENT_SERVICE = getCurrentService(); const STORAGE_KEY = `blacklist_${CURRENT_SERVICE}_v2`; let BL = {}; let initialized = false; let SHOW_HIDDEN = false; let lastHiddenCount = 0; const SELECTORS = { USER_CARD: 'a.user-card[href*="/user/"]', POST_CARD: 'article.post-card, .post-card, li.card-list__item', CONTENT_CONTAINERS: 'main .card-list__items, main, #user-content', }; const qs = (s, r=document) => r.querySelector(s); const qsa = (s, r=document) => Array.from(r.querySelectorAll(s)); function insertEnd(parent, node) { if (parent && node) try { parent.insertAdjacentElement('beforeend', node); } catch {} } function safeDateStamp() { const d = new Date(), pad = n => String(n).padStart(2,'0'); return `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())}_${pad(d.getHours())}-${pad(d.getMinutes())}-${pad(d.getSeconds())}`; } function downloadTextFile(text, filename, mime='application/json;charset=utf-8') { const makeBlobUrl = () => URL.createObjectURL(new Blob([text], { type: mime })); const revokeLater = url => setTimeout(() => { try { URL.revokeObjectURL(url); } catch {} }, 20000); try { const url = makeBlobUrl(), details = { url, name: filename, saveAs: true }; if (typeof GM !== 'undefined' && GM.download) { GM.download(details); revokeLater(url); return; } if (typeof GM_download === 'function') { GM_download(details); revokeLater(url); return; } URL.revokeObjectURL(url); } catch {} try { const url = makeBlobUrl(), a = document.createElement('a'); a.href = url; a.download = filename; a.rel = 'noopener'; a.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true, view: window })); revokeLater(url); } catch (e) { alert('Could not save file automatically. Please enable downloads/pop-ups for this site and try again.'); } } function pickJsonFileText() { return new Promise((resolve, reject) => { const input = document.createElement('input'); input.type = 'file'; input.accept = 'application/json,.json'; input.style.display = 'none'; input.onchange = () => { try { const file = input.files?.[0]; if (!file) return reject(new Error('No file selected')); const reader = new FileReader(); reader.onload = () => resolve(String(reader.result || '')); reader.onerror = () => reject(reader.error || new Error('File reading error')); reader.readAsText(file); } catch (e) { reject(e); } finally { input.remove(); } }; document.body.appendChild(input); input.click(); }); } function mergeImportedObject(obj) { let added = 0, updated = 0, skipped = 0; for (const [key, entry] of Object.entries(obj)) { if (!/^[a-z0-9_-]+:[^:]+$/i.test(key)) { skipped++; continue; } const [svc] = key.split(':'); if (svc.toLowerCase() !== CURRENT_SERVICE) { skipped++; continue; } const record = { service: String(entry.service || svc || ''), user: String(entry.user || ''), label: (String(entry.label || '').trim() || key), addedAt: entry.addedAt || new Date().toISOString() }; if (!BL[key]) added++; else updated++; BL[key] = record; } return { added, updated, skipped }; } async function pickAndImportJsonFile() { const text = await pickJsonFileText(); const obj = JSON.parse(text); if (!obj || typeof obj !== 'object') throw new Error('Invalid JSON format'); const result = mergeImportedObject(obj); await saveBL(); scheduleRefresh(); return result; } function getCreatorKeyFromHref(href) { try { if (!href) return null; const path = href.startsWith('http') ? new URL(href).pathname : new URL(href, location.origin).pathname; const m = path.match(/^\/([a-z0-9_-]+)\/user\/([^\/?#]+)/i); return m ? `${m[1].toLowerCase()}:${decodeURIComponent(m[2])}` : null; } catch { return null; } } const currentAuthorKey = () => getCreatorKeyFromHref(location.pathname); const onAuthorRootPage = () => /^\/([a-z0-9_-]+)\/user\/([^\/?#]+)\/?$/i.test(location.pathname); const hasBL = (key) => key && Object.prototype.hasOwnProperty.call(BL, key); const saveBL = async () => await GM.setValue(STORAGE_KEY, BL); const loadBL = async () => { BL = (await GM.getValue(STORAGE_KEY, {})); if (!BL || typeof BL !== 'object') BL = {}; }; const addToBL = (key, meta={}) => { if (!key) return; const [service, user] = key.split(':'), prev = BL[key] || {}; BL[key] = { service, user, label: (meta.label || prev.label || key).trim(), addedAt: prev.addedAt || new Date().toISOString() }; return saveBL(); }; const removeFromBL = (key) => { if (key) { delete BL[key]; return saveBL(); } }; const formatLabel = (entry) => { if (!entry) return ''; const base = `${entry.service}/${entry.user}`; return (entry.label && entry.label !== base) ? `${entry.label} (${base})` : base; }; let cssInserted = false; function insertStyles() { if (cssInserted) return; cssInserted = true; const style = document.createElement('style'); // --- ИЗМЕНЕННЫЙ БЛОК CSS --- // Убрано правило ".kcbl-soft.kcbl-hidden", которое ломало верстку style.textContent = ` .kcbl-rel { position: relative !important; } .kcbl-btn { display: inline-flex; align-items: center; gap: 0px; padding: 0px; background-color: transparent !important; border: transparent !important; color: #fff; font-weight: 700; text-shadow: hsl(0,0%,0%) 0 0 3px, hsl(0,0%,0%) -1px -1px 0, hsl(0,0%,0%) 1px 1px 0; cursor: pointer; user-select: none; } .kcbl-btn.kcbl--un { color: #ff0000; } .kcbl-btn__icon { width: 22px; height: 22px; display: block; } .kcbl-btn__label { line-height: 1; } .kcbl-inline-btn { position: absolute; top: 6px; right: 6px; padding: 2px 4px; } .kcbl-inline-btn .kcbl-btn__icon { width: 24px; height: 24px; } .kcbl-inline-btn .kcbl-btn__label { display: none; } .kcbl-soft { opacity: 0.35 !important; filter: grayscale(0.2); position: relative; } .kcbl-soft::after { content: 'Hidden'; position: absolute; top: 6px; left: 6px; font-size: 11px; padding: 2px 6px; background: rgba(0,0,0,0.6); color: #fff; border: 1px solid rgba(255,255,255,0.2); border-radius: 999px; z-index: 3; } .kcbl-hidden { display: none !important; } #kcbl-reveal-toggle { position: fixed; bottom: 60px; right: 16px; z-index: 999999; padding: 8px 10px; font-size: 13px; border-radius: 8px; border: 1px solid #0003; background: #202020e0; color: #fff; cursor: pointer; box-shadow: 0 6px 16px #00000059; } #kcbl-fab { position: fixed; bottom: 16px; right: 16px; z-index: 999999; display: inline-flex; align-items: center; gap: 6px; padding: 8px 12px; font-size: 13px; font-weight: 600; border-radius: 999px; border: 1px solid #0003; background: #202020eb; color: #fff; cursor: pointer; box-shadow: 0 6px 16px #00000059; user-select: none; } .kcbl-fab__icon { width: 18px; height: 18px; display: block; } #kcbl-badge { position: fixed; bottom: 42px; right: 12px; z-index: 1000000; min-width: 18px; height: 18px; display: flex; align-items: center; justify-content: center; padding: 0 5px; border-radius: 999px; background-color: #d93025; color: white; font-size: 11px; font-weight: 600; line-height: 1; box-shadow: 0 1px 3px #0000004d; pointer-events: none; } .kcbl-badge--hidden { display: none !important; } .kcbl-modal-link { color: #8ab4f8; text-decoration: none; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .kcbl-modal-link:hover { text-decoration: underline; } `; // --- КОНЕЦ ИЗМЕНЕННОГО БЛОКА CSS --- insertEnd(document.head, style); } const blacklistIconSvg = (klass) => `<svg class="${klass}" viewBox="0 0 24 24"><circle cx="12" cy="12" r="9" fill="none" stroke="currentColor" stroke-width="2"></circle><line x1="6.8" y1="6.8" x2="17.2" y2="17.2" stroke="currentColor" stroke-width="2"></line></svg>`; function applyBLBtnVisual(btn, inBL) { btn.classList.add('kcbl-btn'); btn.classList.toggle('kcbl--un', inBL); btn.innerHTML = `${blacklistIconSvg('kcbl-btn__icon')}<span class="kcbl-btn__label">${inBL ? 'Unblacklist' : 'Blacklist'}</span>`; btn.title = inBL ? 'Remove from blacklist' : 'Add to blacklist'; } function ensureBlacklistToggleButton() { const key = currentAuthorKey(), actions = qs('.user-header__actions'); if (!actions || !key) return; let btn = qs('#kcbl-toggle'); if (!btn) { const favBtn = [...actions.querySelectorAll('button,a')].find(el => el.textContent?.trim().toLowerCase().includes('favorite')); btn = document.createElement('button'); btn.id = 'kcbl-toggle'; btn.type = 'button'; btn.style.marginLeft = '8px'; btn.onclick = async (e) => { e.preventDefault(); const k = currentAuthorKey(); if (!k) return; if (hasBL(k)) { await removeFromBL(k); } else { const nameEl = qs('.user-header__name [itemprop="name"], .user-header__name'); await addToBL(k, { label: nameEl ? nameEl.textContent.trim() : k.replace(':','/') }); } applyBLBtnVisual(btn, hasBL(k)); scheduleRefresh(); }; favBtn ? favBtn.insertAdjacentElement('afterend', btn) : insertEnd(actions, btn); } applyBLBtnVisual(btn, hasBL(key)); } function ensureInlineButtonsForAuthorCards() { qsa(SELECTORS.USER_CARD).forEach(card => { const key = getCreatorKeyFromHref(card.getAttribute('href')); if (!key) return; card.classList.add('kcbl-rel'); let btn = card.querySelector('.kcbl-inline-btn'); if (!btn) { btn = document.createElement('button'); btn.type = 'button'; btn.className = 'kcbl-inline-btn kcbl-btn'; btn.onclick = async (e) => { e.preventDefault(); e.stopPropagation(); if (hasBL(key)) { await removeFromBL(key); } else { const nameEl = card.querySelector('.user-card__name'); await addToBL(key, { label: nameEl ? nameEl.textContent.trim() : key.replace(':','/') }); } applyBLBtnVisual(btn, hasBL(key)); scheduleRefresh(); }; insertEnd(card, btn); } applyBLBtnVisual(btn, hasBL(key)); }); } function ensureUI(type) { let el = qs(`#kcbl-${type}`); if (!el) { el = document.createElement('button'); el.id = `kcbl-${type}`; if (type === 'reveal-toggle') { el.onclick = () => { SHOW_HIDDEN = !SHOW_HIDDEN; scheduleRefresh(); }; } else { // fab const serviceName = CURRENT_SERVICE.charAt(0).toUpperCase() + CURRENT_SERVICE.slice(1); el.innerHTML = `${blacklistIconSvg('kcbl-fab__icon')} <span>${serviceName} Blacklist</span>`; el.title = `Open ${serviceName} blacklist manager`; el.onclick = (e) => { e.preventDefault(); showBlacklistModal(); }; } insertEnd(document.body, el); } return el; } function ensureBlacklistBadge() { let badge = qs('#kcbl-badge'); if (!badge) { badge = document.createElement('div'); badge.id = 'kcbl-badge'; badge.className = 'kcbl-badge--hidden'; insertEnd(document.body, badge); } } function updateUI() { ensureUI('fab'); const badge = qs('#kcbl-badge'), revealBtn = ensureUI('reveal-toggle'); if (onAuthorRootPage() || lastHiddenCount === 0) { revealBtn.style.display = 'none'; } else { revealBtn.style.display = ''; revealBtn.textContent = `${SHOW_HIDDEN ? 'Hide' : 'Show'} hidden (${lastHiddenCount})`; } if (badge) { const count = Object.keys(BL).length; badge.textContent = count; badge.classList.toggle('kcbl-badge--hidden', count === 0); } } function refreshHiding() { if (onAuthorRootPage()) { qsa('.kcbl-hidden, .kcbl-soft').forEach(el => el.classList.remove('kcbl-hidden', 'kcbl-soft')); lastHiddenCount = 0; return; } const allCards = qsa(`${SELECTORS.USER_CARD}, ${SELECTORS.POST_CARD}`); for (const card of allCards) { let key = null; if (card.matches(SELECTORS.USER_CARD)) { key = getCreatorKeyFromHref(card.getAttribute('href')); } else { const anchor = card.querySelector('a[href*="/user/"]'); if (anchor) { key = getCreatorKeyFromHref(anchor.getAttribute('href')); } } const shouldHide = hasBL(key); // --- ИЗМЕНЕННАЯ ЛОГИКА --- // Теперь классы .kcbl-hidden и .kcbl-soft не конфликтуют if (shouldHide) { if (SHOW_HIDDEN) { card.classList.remove('kcbl-hidden'); card.classList.add('kcbl-soft'); } else { card.classList.remove('kcbl-soft'); card.classList.add('kcbl-hidden'); } } else { card.classList.remove('kcbl-hidden', 'kcbl-soft'); } // --- КОНЕЦ ИЗМЕНЕННОЙ ЛОГИКИ --- } lastHiddenCount = qsa('.kcbl-hidden, .kcbl-soft').length; } let rafScheduled = false; function scheduleRefresh() { if (rafScheduled) return; rafScheduled = true; requestAnimationFrame(() => { rafScheduled = false; insertStyles(); ensureBlacklistToggleButton(); ensureInlineButtonsForAuthorCards(); ensureBlacklistBadge(); refreshHiding(); updateUI(); }); } function showBlacklistModal() { const overlay = document.createElement('div'); overlay.style.cssText = 'position:fixed;inset:0;background:#0009;z-index:999999;display:flex;align-items:center;justify-content:center;padding:16px;'; const modal = document.createElement('div'); modal.style.cssText = 'background:#111;color:#eee;max-width:720px;width:100%;display:flex;flex-direction:column;max-height:90vh;border-radius:10px;padding:16px;box-shadow:0 10px 30px #00000080;font-family:system-ui,sans-serif;'; const list = document.createElement('div'); list.style.cssText = 'overflow-y:auto;margin-bottom:12px;'; const onEsc = (e) => { if (e.key === 'Escape') closeModal(); }; const closeModal = () => { document.removeEventListener('keydown', onEsc, true); overlay.remove(); }; document.addEventListener('keydown', onEsc, true); overlay.onclick = (e) => { if (e.target === overlay) closeModal(); }; const serviceName = CURRENT_SERVICE.charAt(0).toUpperCase() + CURRENT_SERVICE.slice(1); const renderList = () => { list.innerHTML = ''; const entries = Object.entries(BL).sort((a,b) => formatLabel(a[1]).localeCompare(formatLabel(b[1]))); (modal.querySelector('#kcbl-modal-title') || {}).textContent = `${serviceName} Blacklist (${entries.length})`; if (entries.length === 0) { list.textContent = 'Blacklist is empty.'; return; } for (const [key, entry] of entries) { const row = document.createElement('div'); row.style.cssText = 'display:flex;align-items:center;justify-content:space-between;gap:8px;padding:6px 0;border-bottom:1px solid #ffffff14;'; const authorLink = document.createElement('a'); authorLink.className = 'kcbl-modal-link'; authorLink.href = `/${entry.service}/user/${encodeURIComponent(entry.user)}`; authorLink.target = '_blank'; authorLink.textContent = authorLink.title = formatLabel(entry); const rmBtn = document.createElement('button'); rmBtn.textContent = 'Remove'; rmBtn.style.cssText = 'padding:4px 8px;border-radius:6px;border:1px solid #333;background:#222;color:#fff;cursor:pointer;'; rmBtn.onclick = async () => { await removeFromBL(key); scheduleRefresh(); renderList(); }; row.append(authorLink, rmBtn); list.append(row); } }; const searchInput = document.createElement('input'); searchInput.placeholder = 'Search by name or ID...'; searchInput.style.cssText = 'width:100%;box-sizing:border-box;padding:8px;background:#222;border:1px solid #444;color:#fff;border-radius:6px;margin-bottom:10px;'; searchInput.oninput = () => { const query = searchInput.value.toLowerCase().trim(); list.querySelectorAll('div[style*="display:flex"]').forEach(row => { row.style.display = row.textContent.toLowerCase().includes(query) ? 'flex' : 'none'; }); }; const title = document.createElement('div'); title.id = 'kcbl-modal-title'; title.style.cssText = 'font-weight:700;font-size:18px;margin-bottom:10px;'; const controls = document.createElement('div'); controls.style.cssText = 'display:flex;gap:8px;margin-top:auto;flex-wrap:wrap;'; ['Close', 'Export JSON', 'Import JSON'].forEach(label => { const btn = document.createElement('button'); btn.textContent = label; btn.style.cssText = 'padding:6px 10px;border-radius:6px;border:1px solid #333;background:#222;color:#fff;cursor:pointer;'; if (label === 'Close') btn.onclick = closeModal; if (label === 'Export JSON') btn.onclick = () => downloadTextFile(JSON.stringify(BL, null, 2), `${CURRENT_SERVICE}_blacklist_${safeDateStamp()}.json`); if (label === 'Import JSON') btn.onclick = async () => { try { const { added, updated, skipped } = await pickAndImportJsonFile(); alert(`Import complete for ${serviceName}.\nAdded: ${added}, Updated: ${updated}, Skipped (wrong service or invalid): ${skipped}.`); renderList(); } catch (e) { alert('Import error: ' + (e.message || e)); } }; controls.append(btn); }); modal.append(title, searchInput, list, controls); overlay.append(modal); insertEnd(document.body, overlay); renderList(); } let observer = null; function startObserver() { if (observer) return; const targetNode = qs(SELECTORS.CONTENT_CONTAINERS); observer = new MutationObserver(() => scheduleRefresh()); observer.observe(targetNode || document.body, { childList: true, subtree: true }); window.addEventListener('popstate', scheduleRefresh, { passive: true }); } async function init() { if (initialized) return; initialized = true; await loadBL(); scheduleRefresh(); startObserver(); } if (document.readyState === 'complete' || document.readyState === 'interactive') { init(); } else { document.addEventListener('DOMContentLoaded', init, { once: true }); } })();