您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Hide posts and authors in lists/search on kemono.cr and coomer.st. Export/import blacklist as JSON file.
当前为
// ==UserScript== // @name Kemono/Coomer Blacklist with backup // @namespace https://tampermonkey.net/ // @version 1.1.0 // @description Hide posts and authors in lists/search on kemono.cr and coomer.st. Export/import blacklist as JSON 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.registerMenuCommand // @grant GM.download // @grant GM_download // @license MIT // @homepageURL https://boosty.to/glauthentica // @contributionURL https://boosty.to/glauthentica // ==/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)); // === 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())}`; } function downloadTextFile(text, filename, mime='application/json;charset=utf-8') { const blob = new Blob([text], { type: mime }); const url = URL.createObjectURL(blob); const details = { url, name: filename, saveAs: true }; try { const dl = (typeof GM !== 'undefined' && GM && typeof GM.download === 'function') ? GM.download : (typeof GM_download === 'function' ? GM_download : null); if (dl) { dl(details); } else { const a = document.createElement('a'); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); a.remove(); } } catch (e) { const a = document.createElement('a'); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); a.remove(); } finally { setTimeout(() => URL.revokeObjectURL(url), 20000); } } 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 = () => { const file = input.files && input.files[0]; if (!file) { reject(new Error('Файл не выбран')); input.remove(); return; } const reader = new FileReader(); reader.onload = () => { resolve(String(reader.result || '')); input.remove(); }; reader.onerror = () => { reject(reader.error || new Error('Ошибка чтения файла')); input.remove(); }; reader.readAsText(file); }; 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, 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 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 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 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; } .kcbl-inline-btn { position: absolute; top: 6px; right: 6px; width: 24px; height: 24px; display: flex; align-items: center; justify-content: center; background: rgba(0,0,0,0.55); color: #fff; border: 1px solid rgba(255,255,255,0.25); border-radius: 50%; font-size: 14px; line-height: 1; cursor: pointer; z-index: 3; user-select: none; } .kcbl-inline-btn:hover { background: rgba(0,0,0,0.75); } .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; } #kcbl-reveal-toggle { position: fixed; bottom: 16px; 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); } `; (document.head || document.documentElement).appendChild(style); } // 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; font-size: 18px; line-height: 1; padding: 4px 6px; border: 1px solid rgba(0,0,0,0.2); border-radius: 6px; background: transparent; cursor: pointer; `; 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 { actions.appendChild(btn); } } updateToggleVisual(btn, key); } function updateToggleVisual(btn, key) { const inBL = hasBL(key); btn.textContent = inBL ? '✅' : '🚫'; btn.title = inBL ? 'Remove from blacklist' : 'Add to blacklist'; } // 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'; 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(); }); card.appendChild(btn); } updateInlineBtnVisual(btn, key); } } function updateInlineBtnVisual(btn, key) { const inBL = hasBL(key); btn.textContent = inBL ? '✅' : '🚫'; btn.title = inBL ? 'Remove from blacklist' : 'Add to blacklist'; } // 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(); }); (document.body || document.documentElement).appendChild(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'; } // 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(); }); } // 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(); }); right.appendChild(openBtn); right.appendChild(rmBtn); row.appendChild(left); row.appendChild(right); list.appendChild(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('Экспорт начат: файл будет сохранен в вашу папку загрузок (или вас попросят выбрать место).'); }); 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}${skipped ? ', пропущено: ' + skipped : ''}.`); overlay.remove(); } catch (e) { alert('Ошибка импорта: ' + (e && e.message ? e.message : e)); } }); controls.appendChild(btnClose); controls.appendChild(btnExport); controls.appendChild(btnImportFile); controls.appendChild(btnImportPaste); modal.appendChild(title); modal.appendChild(list); modal.appendChild(controls); overlay.appendChild(modal); document.body.appendChild(overlay); } function setupMenu() { GM.registerMenuCommand(`Show blacklist (${Object.keys(BL).length})`, showBlacklistModal); GM.registerMenuCommand('Export blacklist (JSON)', async () => { const json = JSON.stringify(BL, null, 2); const filename = `kemono_coomer_blacklist_${safeDateStamp()}.json`; downloadTextFile(json, filename); alert('Экспорт начат: файл будет сохранен в вашу папку загрузок (или вас попросят выбрать место).'); }); GM.registerMenuCommand('Import blacklist from JSON file', async () => { try { const { added, updated, skipped } = await pickAndImportJsonFile(); alert(`Импорт завершён. Добавлено: ${added}, обновлено: ${updated}${skipped ? ', пропущено: ' + skipped : ''}.`); } catch (e) { alert('Ошибка импорта: ' + (e && e.message ? e.message : e)); } }); GM.registerMenuCommand('Import blacklist (paste JSON)', async () => { const text = prompt('Вставьте JSON с чёрным списком:'); if (!text) return; try { const { added, updated, skipped } = await importFromJsonText(text); alert(`Импорт завершён. Добавлено: ${added}, обновлено: ${updated}${skipped ? ', пропущено: ' + skipped : ''}.`); } catch (e) { alert('Ошибка импорта: ' + (e && e.message ? e.message : e)); } }); } 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(); setupMenu(); insertStyles(); scheduleRefresh(); startObserver(); setTimeout(() => saveBL(), 3000); } init(); })();