您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Hide posts and authors in lists/search on kemono.cr and coomer.st. Backup/import your Blacklist.
当前为
// ==UserScript== // @name Kemono/Coomer Blacklist with backup // @version 1.2 // @description Hide posts and authors in lists/search on kemono.cr and coomer.st. Backup/import your Blacklist. // @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 STORAGE_KEY = 'kemono_coomer_blacklist_v1'; let BL = {}; // { "service:user": {service,user,label,addedAt} } let initialized = false; let SHOW_HIDDEN = false; // reveal hidden items softly let lastHiddenCount = 0; const qs = (s, r=document) => r.querySelector(s); const qsa = (s, r=document) => Array.from(r.querySelectorAll(s)); // Safer insertion helper: avoids appendChild function insertEnd(parent, node) { if (!parent || !node) return; try { parent.insertAdjacentElement('beforeend', node); } catch {} } // === Helpers: dates, download, file-pick, import/merge === function safeDateStamp() { const d = new Date(); const 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())}`; } // Без appendChild/a.click — безопасно для сайтов, которые патчат DOM-методы 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); // 1) Tampermonkey-путь try { const url = makeBlobUrl(); const details = { url, name: filename, saveAs: true }; if (typeof GM !== 'undefined' && GM && typeof GM.download === 'function') { GM.download(details); revokeLater(url); return; } if (typeof GM_download === 'function') { GM_download(details); revokeLater(url); return; } URL.revokeObjectURL(url); } catch {} // 2) Открыть blob-URL в новом окне/вкладке (часто запускает скачивание) try { const url = makeBlobUrl(); const win = window.open(url, '_blank', 'noopener'); if (!win) { // Попытка инициировать загрузку синтетическим кликом без вставки в DOM const 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); return; } catch {} // 3) Последний вариант — буфер обмена или окно с JSON (async () => { try { await navigator.clipboard.writeText(String(text)); alert('Не удалось сохранить файл автоматически. JSON скопирован в буфер обмена.'); } catch { const win = window.open('', '_blank'); if (win && win.document) { const esc = s => s.replace(/[&<>]/g, c => ({'&':'&','<':'<','>':'>'}[c])); win.document.write('<pre style="white-space:pre-wrap">' + esc(String(text)) + '</pre>'); win.document.close(); alert('Открылось окно с JSON — сохраните его вручную (Файл → Сохранить как…).'); } else { alert('Не удалось сохранить файл. Скопируйте JSON из следующего окна:\n\n' + String(text)); } } })(); } // Без добавления input в DOM (с запасным вариантом не используется) 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 && input.files[0]; if (!file) { reject(new Error('Файл не выбран')); return; } const reader = new FileReader(); reader.onload = () => resolve(String(reader.result || '')); reader.onerror = () => reject(reader.error || new Error('Ошибка чтения файла')); reader.readAsText(file); } catch (e) { reject(e); } }; try { // Большинство браузеров позволяют click() без вставки в DOM input.click(); } catch (e) { // Если браузер не даёт кликнуть без DOM — сообщим об ошибке reject(new Error('Браузер блокирует выбор файла. Разрешите всплывающие окна или попробуйте другой браузер.')); } }); } 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, usr] = key.split(':'); const record = { service: (entry.service || svc || '').toString(), user: (entry.user || usr || '').toString(), label: ((entry.label && String(entry.label).trim()) || `${svc}/${usr}`), addedAt: entry.addedAt || new Date().toISOString() }; if (BL[key]) updated++; else added++; BL[key] = record; } return { added, updated, skipped }; } async function importFromJsonText(text) { const obj = JSON.parse(text); if (!obj || typeof obj !== 'object') throw new Error('Неверный формат JSON'); const res = mergeImportedObject(obj); await saveBL(); scheduleRefresh(); return res; } async function pickAndImportJsonFile() { const text = await pickJsonFileText(); return importFromJsonText(text); } // === end helpers === function parseUrlToPathname(href) { try { if (!href) return ''; if (href.startsWith('http')) return new URL(href).pathname; return new URL(href, location.origin).pathname; } catch { return ''; } } function getCreatorKeyFromPath(pathname) { try { const m = pathname.match(/^\/([a-z0-9_-]+)\/user\/([^\/?#]+)(?:\/?|$)/i); if (!m) return null; const service = m[1].toLowerCase(); const user = decodeURIComponent(m[2]); return `${service}:${user}`; } catch { return null; } } function getCreatorKeyFromHref(href) { const path = parseUrlToPathname(href); return getCreatorKeyFromPath(path); } function currentAuthorKey() { return getCreatorKeyFromPath(location.pathname); } function onAuthorRootPage() { return /^\/([a-z0-9_-]+)\/user\/([^\/?#]+)\/?$/i.test(location.pathname); } function hasBL(key) { return key && Object.prototype.hasOwnProperty.call(BL, key); } async function saveBL() { await GM.setValue(STORAGE_KEY, BL); } async function loadBL() { BL = await GM.getValue(STORAGE_KEY, {}); if (!BL || typeof BL !== 'object') BL = {}; } function addToBL(key, meta={}) { if (!key) return; const [service, user] = key.split(':'); const now = new Date().toISOString(); const prev = BL[key] || {}; BL[key] = { service, user, label: (meta.label || prev.label || `${service}/${user}`).trim(), addedAt: prev.addedAt || now }; return saveBL(); } function removeFromBL(key) { if (!key) return; delete BL[key]; return saveBL(); } function formatLabel(entry) { if (!entry) return ''; const base = `${entry.service}/${entry.user}`; return entry.label && entry.label !== base ? `${entry.label} (${base})` : base; } // Styles let cssInserted = false; function insertStyles() { if (cssInserted) return; cssInserted = true; const style = document.createElement('style'); style.textContent = ` .kcbl-rel { position: relative !important; } /* Favorite-like buttons */ .kcbl-btn { display: inline-flex; align-items: center; gap: 0px; padding: 0px 0px; background-color: transparent !important; border: transparent !important; color: #fff; font-weight: 700; text-shadow: hsl(0, 0%, 0%) 0px 0px 3px, hsl(0, 0%, 0%) -1px -1px 0px, hsl(0, 0%, 0%) 1px 1px 0px; cursor: pointer; user-select: none; } .kcbl-btn.kcbl--un { color: #ff0000; } .kcbl-btn__icon { width: 22px; height: 22px; display: block; color: currentColor; } .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; line-height: 1; 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; } /* Floating "Show hidden" moved higher to avoid overlap with Blacklist FAB */ #kcbl-reveal-toggle { position: fixed; bottom: 60px; right: 16px; z-index: 999999; padding: 8px 10px; font-size: 13px; border-radius: 8px; border: 1px solid rgba(0,0,0,0.25); background: rgba(32,32,32,0.9); color: #fff; cursor: pointer; box-shadow: 0 6px 16px rgba(0,0,0,0.35); } #kcbl-reveal-toggle:hover { background: rgba(32,32,32,1); } /* Floating Blacklist button (FAB) */ #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 rgba(0,0,0,0.25); background: rgba(32,32,32,0.92); color: #fff; cursor: pointer; box-shadow: 0 6px 16px rgba(0,0,0,0.35); user-select: none; } #kcbl-fab:hover { background: rgba(32,32,32,1); } #kcbl-fab .kcbl-fab__icon { width: 18px; height: 18px; color: currentColor; display: block; } `; const head = document.head || document.querySelector('head') || document.documentElement; insertEnd(head, style); } // SVG icon (ban/prohibited), uses currentColor function blacklistIconSvg() { return ` <svg class="kcbl-btn__icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"> <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 blacklistIconSmall() { return ` <svg class="kcbl-fab__icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"> <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()}<span class="kcbl-btn__label">${inBL ? 'Unblacklist' : 'Blacklist'}</span>`; btn.title = inBL ? 'Remove from blacklist' : 'Add to blacklist'; } // Header toggle button (author page) function ensureBlacklistToggleButton() { const key = currentAuthorKey(); const actions = qs('.user-header__actions'); if (!actions || !key) return; let btn = qs('#kcbl-toggle'); if (!btn) { let favoriteBtn = null; for (const el of actions.querySelectorAll('button,a')) { const txt = (el.textContent || '').trim().toLowerCase(); if (txt.includes('favorite')) { favoriteBtn = el; break; } } btn = document.createElement('button'); btn.id = 'kcbl-toggle'; btn.type = 'button'; btn.style.cssText = 'margin-left: 8px;'; btn.addEventListener('click', 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'); const label = nameEl ? nameEl.textContent.trim() : k.replace(':','/'); await addToBL(k, { label }); } updateToggleVisual(btn, currentAuthorKey()); scheduleRefresh(); }); if (favoriteBtn && favoriteBtn.parentElement === actions) { favoriteBtn.insertAdjacentElement('afterend', btn); } else { insertEnd(actions, btn); } } updateToggleVisual(btn, key); } function updateToggleVisual(btn, key) { const inBL = hasBL(key); applyBLBtnVisual(btn, inBL); } // Mini-buttons on author cards function ensureInlineButtonsForAuthorCards() { const cards = qsa('a.user-card[href*="/user/"]'); for (const card of cards) { let key = card.dataset.kcblKey; if (!key) { const svc = card.dataset.service; const id = card.dataset.id; if (svc && id) key = `${svc}:${id}`; else key = getCreatorKeyFromHref(card.getAttribute('href')); if (key) card.dataset.kcblKey = key; } if (!key) continue; if (!card.classList.contains('kcbl-rel')) 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.addEventListener('click', async (e) => { e.preventDefault(); e.stopPropagation(); const k = card.dataset.kcblKey || key; if (!k) return; if (hasBL(k)) { await removeFromBL(k); } else { const nameEl = card.querySelector('.user-card__name'); const label = nameEl ? nameEl.textContent.trim() : k.replace(':','/'); await addToBL(k, { label }); } updateInlineBtnVisual(btn, k); scheduleRefresh(); }); insertEnd(card, btn); } updateInlineBtnVisual(btn, key); } } function updateInlineBtnVisual(btn, key) { const inBL = hasBL(key); applyBLBtnVisual(btn, inBL); } // Floating "Show hidden" button function ensureRevealToggleButton() { let btn = qs('#kcbl-reveal-toggle'); if (!btn) { btn = document.createElement('button'); btn.id = 'kcbl-reveal-toggle'; btn.addEventListener('click', () => { SHOW_HIDDEN = !SHOW_HIDDEN; scheduleRefresh(); }); insertEnd(document.body || document.documentElement, btn); } updateRevealToggleButton(); } function updateRevealToggleButton() { const btn = qs('#kcbl-reveal-toggle'); if (!btn) return; const onAuthorPage = onAuthorRootPage(); if (onAuthorPage || lastHiddenCount === 0) { btn.style.display = 'none'; return; } btn.style.display = ''; btn.textContent = SHOW_HIDDEN ? `Hide hidden (${lastHiddenCount})` : `Show hidden (${lastHiddenCount})`; btn.title = SHOW_HIDDEN ? 'Turn off revealing hidden cards/posts' : 'Turn on revealing hidden cards/posts'; } // Floating Blacklist FAB button function ensureBlacklistFAB() { let fab = qs('#kcbl-fab'); if (!fab) { fab = document.createElement('button'); fab.id = 'kcbl-fab'; fab.type = 'button'; fab.innerHTML = `${blacklistIconSmall()} <span>Blacklist</span>`; fab.title = 'Open blacklist manager'; fab.addEventListener('click', (e) => { e.preventDefault(); showBlacklistModal(); }); insertEnd(document.body || document.documentElement, fab); } } // Hiding logic function hideContainer(el, shouldHide) { if (!el) return; if (shouldHide) { el.classList.add('kcbl-hidden'); if (SHOW_HIDDEN) { el.classList.add('kcbl-soft'); el.style.display = ''; } else { el.classList.remove('kcbl-soft'); el.style.display = 'none'; } } else { el.classList.remove('kcbl-hidden'); el.classList.remove('kcbl-soft'); el.style.display = ''; } } function refreshHiding() { try { insertStyles(); // Do not hide anything on author root page if (onAuthorRootPage()) { for (const el of qsa('.kcbl-hidden, .kcbl-soft')) hideContainer(el, false); lastHiddenCount = 0; updateRevealToggleButton(); return; } let hiddenCount = 0; // Author cards const authorCards = qsa('a.user-card[href*="/user/"]'); for (const card of authorCards) { let key = card.dataset.kcblKey; if (!key) { const svc = card.dataset.service; const id = card.dataset.id; if (svc && id) key = `${svc}:${id}`; else key = getCreatorKeyFromHref(card.getAttribute('href')); if (key) card.dataset.kcblKey = key; } if (!key) continue; // Update label guess if missing if (hasBL(key)) { const entry = BL[key]; const base = `${entry.service}/${entry.user}`; if (!entry.label || entry.label === base) { const nameEl = card.querySelector('.user-card__name'); const guess = nameEl ? nameEl.textContent.trim() : ''; if (guess) entry.label = guess; } } const toHide = hasBL(key); hideContainer(card, toHide); if (toHide) hiddenCount++; } // Post cards const postCards = qsa('article.post-card, .post-card'); for (const card of postCards) { let key = card.dataset.kcblKey; if (!key) { const svc = card.dataset.service; const usr = card.dataset.user; if (svc && usr) key = `${svc}:${usr}`; } if (!key) { const a = card.querySelector('a[href*="/user/"][href*="/post/"]'); if (a) key = getCreatorKeyFromHref(a.getAttribute('href')); } if (key) card.dataset.kcblKey = key; if (!key) continue; const toHide = hasBL(key); hideContainer(card, toHide); if (toHide) hiddenCount++; } // Fallback: by post anchors const postAnchors = qsa('a[href*="/user/"][href*="/post/"]'); for (const a of postAnchors) { let key = a.dataset.kcblKey || getCreatorKeyFromHref(a.getAttribute('href')); if (key) a.dataset.kcblKey = key; if (!key) continue; let container = a.closest('article.post-card, .post-card, li'); if (!container) { let el = a; for (let i=0; i<6 && el && el !== document.body; i++) { if (/post-card/i.test(el.className || '') || el.tagName === 'ARTICLE' || el.tagName === 'LI') { container = el; break; } el = el.parentElement; } } if (!container) continue; const toHide = hasBL(key); hideContainer(container, toHide); if (toHide) hiddenCount++; } lastHiddenCount = hiddenCount; ensureRevealToggleButton(); ensureInlineButtonsForAuthorCards(); updateRevealToggleButton(); } catch (e) { console.error('[KC-BL] refreshHiding error:', e); } } let rafScheduled = false; function scheduleRefresh() { if (rafScheduled) return; rafScheduled = true; requestAnimationFrame(() => { rafScheduled = false; ensureBlacklistToggleButton(); refreshHiding(); ensureBlacklistFAB(); }); } // Blacklist modal (view/export/import) function showBlacklistModal() { const overlay = document.createElement('div'); overlay.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.6);z-index:999999;display:flex;align-items:center;justify-content:center;'; const modal = document.createElement('div'); modal.style.cssText = 'background:#111;color:#eee;max-width:720px;width:90%;max-height:80vh;overflow:auto;border-radius:10px;padding:16px;box-shadow:0 10px 30px rgba(0,0,0,0.5);font-family:system-ui,-apple-system,"Segoe UI",Roboto,Arial,sans-serif;'; const title = document.createElement('div'); title.textContent = `Blacklist (${Object.keys(BL).length})`; title.style.cssText = 'font-weight:700;font-size:18px;margin-bottom:10px;'; const list = document.createElement('div'); if (Object.keys(BL).length === 0) { list.textContent = 'Blacklist is empty.'; list.style.margin = '8px 0 12px'; } else { for (const [key, entry] of Object.entries(BL)) { 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 rgba(255,255,255,0.08);'; const left = document.createElement('div'); left.textContent = formatLabel(entry); left.style.cssText = 'white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:75%;'; const right = document.createElement('div'); const openBtn = document.createElement('a'); openBtn.href = `/${entry.service}/user/${encodeURIComponent(entry.user)}`; openBtn.textContent = 'Open'; openBtn.target = '_blank'; openBtn.style.cssText = 'color:#8ab4f8;text-decoration:none;margin-right:10px;'; 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.addEventListener('click', async () => { await removeFromBL(key); row.remove(); title.textContent = `Blacklist (${Object.keys(BL).length})`; scheduleRefresh(); }); insertEnd(right, openBtn); insertEnd(right, rmBtn); insertEnd(row, left); insertEnd(row, right); insertEnd(list, row); } } const controls = document.createElement('div'); controls.style.cssText = 'display:flex;gap:8px;margin-top:12px;flex-wrap:wrap;'; const btnClose = document.createElement('button'); btnClose.textContent = 'Close'; btnClose.style.cssText = 'padding:6px 10px;border-radius:6px;border:1px solid #333;background:#222;color:#fff;cursor:pointer;'; btnClose.addEventListener('click', () => overlay.remove()); const btnExport = document.createElement('button'); btnExport.textContent = 'Export JSON'; btnExport.style.cssText = 'padding:6px 10px;border-radius:6px;border:1px solid #333;background:#222;color:#fff;cursor:pointer;'; btnExport.addEventListener('click', async () => { const json = JSON.stringify(BL, null, 2); const filename = `kemono_coomer_blacklist_${safeDateStamp()}.json`; downloadTextFile(json, filename); alert('Экспорт: файл будет сохранён через Tampermonkey (или откроется новая вкладка/окно).'); }); const btnCopy = document.createElement('button'); btnCopy.textContent = 'Copy JSON'; btnCopy.style.cssText = 'padding:6px 10px;border-radius:6px;border:1px solid #333;background:#222;color:#fff;cursor:pointer;'; btnCopy.addEventListener('click', async () => { try { await navigator.clipboard.writeText(JSON.stringify(BL, null, 2)); alert('JSON скопирован в буфер обмена.'); } catch (e) { alert('Не удалось скопировать: ' + (e && e.message ? e.message : e)); } }); const btnImportFile = document.createElement('button'); btnImportFile.textContent = 'Import JSON file'; btnImportFile.style.cssText = 'padding:6px 10px;border-radius:6px;border:1px solid #333;background:#222;color:#fff;cursor:pointer;'; btnImportFile.addEventListener('click', async () => { try { const { added, updated, skipped } = await pickAndImportJsonFile(); alert(`Импорт завершён. Добавлено: ${added}, обновлено: ${updated}${skipped ? ', пропущено: ' + skipped : ''}.`); overlay.remove(); } catch (e) { alert('Ошибка импорта: ' + (e && e.message ? e.message : e)); } }); const btnImportPaste = document.createElement('button'); btnImportPaste.textContent = 'Import JSON (paste)'; btnImportPaste.style.cssText = 'padding:6px 10px;border-radius:6px;border:1px solid #333;background:#222;color:#fff;cursor:pointer;'; btnImportPaste.addEventListener('click', async () => { const text = prompt('Вставьте JSON с чёрным списком:'); if (!text) return; try { const { added, updated, skipped } = await importFromJsonText(text); alert(`Импорт завершён. Добавлено: ${added}, обновлено: ${updated}${скipped ? ', пропущено: ' + skipped : ''}.`); overlay.remove(); } catch (e) { alert('Ошибка импорта: ' + (e && e.message ? e.message : e)); } }); insertEnd(controls, btnClose); insertEnd(controls, btnExport); insertEnd(controls, btnCopy); insertEnd(controls, btnImportFile); insertEnd(controls, btnImportPaste); insertEnd(modal, title); insertEnd(modal, list); insertEnd(modal, controls); insertEnd(overlay, modal); insertEnd(document.body || document.documentElement, overlay); } // Removed Tampermonkey menu entries (in-page control only) let observer = null; function startObserver() { if (observer) return; observer = new MutationObserver(() => scheduleRefresh()); observer.observe(document.documentElement, { childList: true, subtree: true }); window.addEventListener('popstate', scheduleRefresh, { passive: true }); } async function init() { if (initialized) return; initialized = true; await loadBL(); insertStyles(); ensureBlacklistFAB(); scheduleRefresh(); startObserver(); setTimeout(() => saveBL(), 3000); } init(); })();