// ==UserScript==
// @name Bangumi排行榜条目屏蔽器
// @namespace https://greasyfork.org/zh-CN/scripts/544368
// @version 1.2
// @description 在排行页面右上角增加按钮,可开启屏蔽模式进行条目屏蔽或管理已屏蔽条目,支持本地持久保存。
// @author forary
// @license MIT
// @match https://bgm.tv/*/browser*sort=rank*
// @match https://bangumi.tv/*/browser*sort=rank*
// @match https://chii.in/*/browser*sort=rank*
// @grant none
// ==/UserScript==
(function () {
'use strict';
const category = (function () {
const path = location.pathname;
if (path.startsWith('/anime/')) return 'anime';
if (path.startsWith('/book/')) return 'book';
if (path.startsWith('/music/')) return 'music';
if (path.startsWith('/game/')) return 'game';
if (path.startsWith('/real/')) return 'real';
return 'wrong';
})();
const STORAGE_KEY = `bangumi_rank_blocked_items_${category}`;
const SETTINGS_KEY = 'bangumi_rank_user_settings';
function getUserSettings() {
try {
const raw = localStorage.getItem(SETTINGS_KEY);
const parsed = raw ? JSON.parse(raw) : {};
return {
enableBlocking: parsed.enableBlocking ?? true,
renumberRanks: parsed.renumberRanks ?? true,
};
} catch (e) {
return { enableBlocking: true, renumberRanks: true };
}
}
function setUserSettings(settings) {
localStorage.setItem(SETTINGS_KEY, JSON.stringify(settings));
}
function getBlockedMap() {
const raw = localStorage.getItem(STORAGE_KEY);
return raw ? JSON.parse(raw) : {};
}
function setBlockedMap(map) {
localStorage.setItem(STORAGE_KEY, JSON.stringify(map));
}
function addToBlockedMap(id, name) {
const map = getBlockedMap();
if (!(id in map)) {
map[id] = name;
setBlockedMap(map);
}
}
function removeFromBlockedMap(id) {
const map = getBlockedMap();
if (id in map) {
delete map[id];
setBlockedMap(map);
}
}
// v1.1 判断排行榜是否开启筛选
function isFiltered() {
const pathname = location.pathname;
const searchParams = new URLSearchParams(location.search);
// 未筛选时 pathname 是 /anime/browser 或 /anime/browser/
const isCleanPath = pathname === '/anime/browser' || pathname === '/anime/browser/';
// 判断是否包含额外的 pathname 参数,如分类、时间
const isPathFiltered = !isCleanPath;
// 判断是否含有筛选用的 query,如 orderby
const hasFilterParams = searchParams.has('orderby');
return isPathFiltered || hasFilterParams;
}
// 更改界面,隐藏当前页面中的已屏蔽条目
function blockExistingItems() {
const settings = getUserSettings();
if (settings.enableBlocking === false) {
return;
}
const blockedMap = getBlockedMap();
const items = document.querySelectorAll('li[id^="item_"]'); // 页面中存在的条目列表
items.forEach(item => {
const idMatch = item.id.match(/^item_(\d+)$/); // 提取出数值ID
if (!idMatch) return; // 失败则跳过
const id = idMatch[1];
if (id in blockedMap) {
item.remove();
}
});
if (!isFiltered() && settings.renumberRanks) {
renumberRanks();
}
}
// v1.1 更改Rank数值,避免页内数字间隔
function renumberRanks() {
const items = Array.from(document.querySelectorAll('li[id^="item_"]'));
let rankOffset = (function () {
// 从 URL 中读取 page 参数,未指定时为第一页
const pageMatch = location.search.match(/[?&]page=(\d+)/);
const page = pageMatch ? parseInt(pageMatch[1], 10) : 1;
return (page - 1) * 24;
})();
items.forEach((item, index) => {
const rankElem = item.querySelector('.rank small');
if (rankElem && rankElem.nextSibling?.nodeType === Node.TEXT_NODE) {
rankElem.nextSibling.textContent = (rankOffset + index + 1).toString();
}
});
}
// 显示每个条目的 “屏蔽” 按钮
function showBlockButtons() {
const items = document.querySelectorAll('li[id^="item_"]');
items.forEach(item => {
const idMatch = item.id.match(/^item_(\d+)$/);
if (!idMatch) return;
const id = idMatch[1];
const btn = document.createElement('button');
btn.textContent = '×';
btn.className = 'bangumi-block-btn';
Object.assign(btn.style, {
position: 'absolute',
bottom: '8px',
right: '10px',
width: '18px',
height: '18px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: '16px',
padding: '0',
background: '#f66',
color: '#fff',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
});
btn.onclick = () => {
const nameElem = item.querySelector('.inner h3 a');
const name = nameElem?.textContent?.trim() || '未知标题';
addToBlockedMap(id, name);
item.remove();
};
item.style.position = 'relative';
item.appendChild(btn);
});
}
// 隐藏每个条目的 “屏蔽” 按钮
function hideBlockButtons() {
const buttons = document.querySelectorAll('.bangumi-block-btn');
buttons.forEach(btn => {
btn.parentElement?.removeChild(btn);
});
}
// 两个控制按键
function createControlButtons() {
const toggleBtn = document.createElement('button');
toggleBtn.textContent = '添加屏蔽';
Object.assign(toggleBtn.style, {
position: 'absolute',
right: '90px',
padding: '8px 8px',
background: '#F09199',
color: '#fff',
border: 'none',
borderRadius: '12px',
cursor: 'pointer',
fontSize: '12px',
lineHeight: '1',
});
toggleBtn.onclick = () => {
if (toggleBtn.textContent === '添加屏蔽') {
showBlockButtons();
toggleBtn.textContent = '退出';
} else {
hideBlockButtons();
toggleBtn.textContent = '添加屏蔽';
}
};
const showListBtn = document.createElement('button');
showListBtn.textContent = '管理屏蔽';
Object.assign(showListBtn.style, {
position: 'absolute',
right: '20px',
padding: '8px 8px',
background: '#F09199',
color: '#fff',
border: 'none',
borderRadius: '12px',
cursor: 'pointer',
fontSize: '12px',
lineHeight: '1',
});
showListBtn.onclick = showControlModal;
const header = document.querySelector('#header');
if (header) {
header.style.position = 'relative';
header.appendChild(toggleBtn);
header.appendChild(showListBtn);
}
}
// 打开管理界面
function showControlModal() {
// v1.2 禁止主页面滚动
const scrollBarWidth = window.innerWidth - document.documentElement.clientWidth;
const scrollTop = window.scrollY || document.documentElement.scrollTop;
document.body.style.position = 'fixed';
document.body.style.top = `-${scrollTop}px`;
document.body.style.left = '0';
document.body.style.right = '0';
document.body.style.overflow = 'hidden';
document.body.style.paddingRight = `${scrollBarWidth}px`; // 补偿滚动条宽度
const blocked = getBlockedMap();
const settings = getUserSettings();
// 遮罩层
const overlay = document.createElement('div');
Object.assign(overlay.style, {
position: 'fixed',
top: 0, left: 0, right: 0, bottom: 0,
background: 'rgba(0,0,0,0.6)',
zIndex: 10000,
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
});
// v1.2 点击遮罩层关闭
overlay.addEventListener('click', (e) => {
if (e.target === overlay) {
closeControlModal(overlay, scrollTop);
}
});
// 粉色主体
// 外层 modal
const modalW = document.createElement('div');
Object.assign(modalW.style, {
position: 'relative',
background: '#F09199',
padding: '8px 0px',
borderRadius: '10px',
width: '300px',
maxHeight: '400px',
overflow: 'hidden', // 让内部滚动条被裁剪
});
// 内层内容容器
const modal = document.createElement('div');
Object.assign(modal.style, {
position: 'relative',
padding: '8px 20px',
maxHeight: '400px',
overflowY: 'auto', // 滚动在内部
boxSizing: 'border-box',
scrollbarWidth: 'thin',
scrollbarColor: '#ffb6c1 #f09199' // 滑块颜色 背景颜色
});
// 关闭按键
const closeBtn = document.createElement('button');
closeBtn.textContent = '×';
Object.assign(closeBtn.style, {
width: '22px',
height: '22px',
lineHeight: '22px',
textAlign: 'center',
fontSize: '16px',
background: '#ffb6c1',
color: '#fff',
borderRadius: '50%',
border: 'none',
cursor: 'pointer',
position: 'absolute',
top: '13px',
right: '14px',
transition: 'transform 0.2s ease',
});
closeBtn.onmouseover = () => closeBtn.style.transform = 'scale(1.1)';
closeBtn.onmouseout = () => closeBtn.style.transform = 'scale(1)';
closeBtn.onclick = () => closeControlModal(overlay, scrollTop);
// ===== 控制区域 设置选项 =====
const title1 = document.createElement('h3');
title1.textContent = `设置选项`;
title1.style.marginBottom = '10px';
// v1.2 屏蔽开关
const globalToggleContainer = document.createElement('div');
Object.assign(globalToggleContainer.style, {
marginBottom: '10px',
fontSize: '13px',
display: 'flex',
alignItems: 'center',
gap: '6px',
});
const globalCheckbox = document.createElement('input');
globalCheckbox.type = 'checkbox';
globalCheckbox.checked = settings.enableBlocking ?? true;
globalCheckbox.id = 'enable-blocking-toggle';
globalCheckbox.style.cursor = 'pointer';
const globalLabel = document.createElement('label');
globalLabel.textContent = '启用屏蔽功能';
globalLabel.htmlFor = 'enable-blocking-toggle';
globalCheckbox.onchange = () => {
settings.enableBlocking = globalCheckbox.checked;
setUserSettings(settings);
};
globalToggleContainer.appendChild(globalCheckbox);
globalToggleContainer.appendChild(globalLabel);
// v1.1 重新编号开关
const renumberContainer = document.createElement('div');
Object.assign(renumberContainer.style, {
marginBottom: '15px',
fontSize: '13px',
display: 'flex',
alignItems: 'center',
gap: '6px',
});
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.checked = getUserSettings().renumberRanks;
checkbox.id = 'renumber-toggle';
checkbox.style.cursor = 'pointer';
const label = document.createElement('label');
label.textContent = '重新编号 Rank 排名';
label.htmlFor = 'renumber-toggle'; // label 绑定 input
checkbox.onchange = () => {
settings.renumberRanks = checkbox.checked;
setUserSettings(settings);
};
renumberContainer.appendChild(checkbox);
renumberContainer.appendChild(label);
// ===== 内容区域 屏蔽条目列表 =====
const title2 = document.createElement('h3');
title2.textContent = `已屏蔽${category}条目`;
title2.style.marginBottom = '10px';
const list = document.createElement('ul');
// 列表
for (const [id, name] of Object.entries(blocked)) {
const li = document.createElement('li');
Object.assign(li.style, {
display: 'flex',
alignItems: 'center',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
width: '100%',
marginBottom: '5px',
});
// 创建文本节点(会自动省略过长文本)
const textSpan = document.createElement('span');
textSpan.textContent = `${id} - ${name}`;
Object.assign(textSpan.style, {
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
flexGrow: 1,
});
const unhideBtn = document.createElement('button');
unhideBtn.textContent = '取消屏蔽';
Object.assign(unhideBtn.style, {
marginRight: '8px',
padding: '2px 6px',
fontSize: '12px',
background: '#4CAF50',
color: '#fff',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
});
unhideBtn.onclick = () => {
removeFromBlockedMap(id);
li.remove();
};
li.appendChild(unhideBtn);
li.appendChild(textSpan);
list.appendChild(li);
}
// v1.2 清空屏蔽
const clearBtn = document.createElement('button');
clearBtn.textContent = '清空全部屏蔽';
Object.assign(clearBtn.style, {
marginTop: '5px',
padding: '3px 5px',
fontSize: '12px',
background: '#d9534f',
color: '#fff',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
});
clearBtn.onclick = () => {
if (confirm(`确定清空所有已屏蔽${category}条目吗?`)) {
setBlockedMap({});
list.innerHTML = '';
}
};
modal.appendChild(title1);
modal.appendChild(globalToggleContainer);
modal.appendChild(renumberContainer);
modal.appendChild(title2);
modal.appendChild(list);
modal.appendChild(clearBtn);
modalW.appendChild(modal);
modalW.appendChild(closeBtn);
overlay.appendChild(modalW);
document.body.appendChild(overlay);
}
// 关闭管理界面 v1.2
function closeControlModal(overlay, scrollTop) {
overlay.remove();
document.body.style.position = '';
document.body.style.top = '';
document.body.style.left = '';
document.body.style.right = '';
document.body.style.overflow = '';
document.body.style.paddingRight = '';
if (scrollTop !== undefined) {
window.scrollTo(0, scrollTop);
}
}
function init() {
blockExistingItems();
createControlButtons();
}
window.addEventListener('load', init);
const observer = new MutationObserver(() => {
blockExistingItems();
});
observer.observe(document.body, {
childList: true,
subtree: true
});
})();