// ==UserScript==
// @name Via风格自定义规则拦截器
// @namespace http://tampermonkey.net/
// @version 5.3.0
// @description 完全手动控制折叠/展开,支持域名级和规则级单独禁用,高性能局部更新,导入导出界面完整恢复
// @author Custom Rules Manager
// @match *://*/*
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_addStyle
// @grant GM_registerMenuCommand
// @grant GM_xmlhttpRequest
// @grant unsafeWindow
// @run-at document-start
// @license MIT
// @icon data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNjQiIGhlaWdodD0iNjQiIHZpZXdCb3g9IjAgMCA2NCA2NCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPGNpcmNsZSBjeD0iMzIiIGN5PSIzMiIgcj0iMzAiIGZpbGw9InVybCgjZ3JhZCkiLz4KPHBvbHlsaW5lIHBvaW50cz0iMjQsMjQgMzIsNDAgNDAsMjQiIHN0cm9rZT0id2hpdGUiIHN0cm9rZS13aWR0aD0iNCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBmaWxsPSJub25lIi8+CjxkZWZzPgo8bGluZWFyR3JhZGllbnQgaWQ9ImdyYWQiIHgxPSIwIiB5MT0iMCIgeDI9IjY0IiB5Mj0iNjQiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj4KPHN0b3Agc3RvcC1jb2xvcj0iIzY2N0VFQSIvPgo8c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiM3NjRCQTIiLz4KPC9saW5lYXJHcmFkaWVudD4KPC9kZWZzPgo8L3N2Zz4=
// ==/UserScript==
(function() {
'use strict';
// ========== 存储与数据结构 ==========
const STORAGE_KEY = 'custom_rules_v8';
const STORAGE_VERSION = 5;
let dataCache = null;
let currentDomain = window.location.hostname;
let uiVisible = false;
let shadowRoot = null;
let styleElement = null;
function getDefaultData() {
return {
version: STORAGE_VERSION,
rules: [],
domainStatus: {},
ruleStatus: {}
};
}
function loadData() {
if (dataCache) return dataCache;
const saved = GM_getValue(STORAGE_KEY, null);
if (saved) {
try {
const data = JSON.parse(saved);
if (data.version === STORAGE_VERSION) {
data.ruleStatus = data.ruleStatus || {};
data.domainStatus = data.domainStatus || {};
const validRules = new Set(data.rules);
Object.keys(data.ruleStatus).forEach(r => { if (!validRules.has(r)) delete data.ruleStatus[r]; });
const validDomains = new Set();
data.rules.forEach(r => { const p = parseRule(r); if(p) validDomains.add(p.domain); });
Object.keys(data.domainStatus).forEach(d => { if (!validDomains.has(d)) delete data.domainStatus[d]; });
dataCache = data;
return data;
}
} catch(e) {}
}
// 迁移旧版
const oldKey = 'custom_rules_v7';
const old = GM_getValue(oldKey, null);
if (old) {
try {
const oldData = JSON.parse(old);
if (oldData.rules) {
const newData = {
version: STORAGE_VERSION,
rules: oldData.rules,
domainStatus: oldData.domainStatus || {},
ruleStatus: oldData.ruleStatus || {}
};
newData.rules.forEach(r => { if (newData.ruleStatus[r] === undefined) newData.ruleStatus[r] = true; });
dataCache = newData;
saveData(dataCache);
return dataCache;
}
} catch(e) {}
}
dataCache = getDefaultData();
return dataCache;
}
function saveData(data) {
dataCache = data;
GM_setValue(STORAGE_KEY, JSON.stringify(data));
}
function getAllRules() {
return loadData().rules.slice();
}
function saveRules(rulesArray) {
const data = loadData();
const oldRuleStatus = data.ruleStatus;
const newRuleStatus = {};
rulesArray.forEach(r => { newRuleStatus[r] = oldRuleStatus[r] !== undefined ? oldRuleStatus[r] : true; });
data.rules = rulesArray;
data.ruleStatus = newRuleStatus;
const validDomains = new Set();
rulesArray.forEach(r => { const p = parseRule(r); if(p) validDomains.add(p.domain); });
Object.keys(data.domainStatus).forEach(d => { if (!validDomains.has(d)) delete data.domainStatus[d]; });
validDomains.forEach(d => { if (data.domainStatus[d] === undefined) data.domainStatus[d] = true; });
saveData(data);
applyRules();
if (shadowRoot) refreshManagePanel();
}
function replaceAllRules(rulesArray) {
const data = getDefaultData();
data.rules = rulesArray;
data.ruleStatus = {};
rulesArray.forEach(r => { data.ruleStatus[r] = true; });
const validDomains = new Set();
rulesArray.forEach(r => { const p = parseRule(r); if(p) validDomains.add(p.domain); });
validDomains.forEach(d => { data.domainStatus[d] = true; });
saveData(data);
applyRules();
if (shadowRoot) refreshManagePanel();
}
function isDomainEnabled(domain) {
const data = loadData();
return data.domainStatus[domain] !== false;
}
function setDomainEnabled(domain, enabled) {
const data = loadData();
if (data.domainStatus[domain] === enabled) return;
data.domainStatus[domain] = enabled;
saveData(data);
applyRules();
if (shadowRoot) {
const group = shadowRoot.querySelector(`.domain-group[data-domain="${CSS.escape(domain)}"]`);
if (group) {
const isCurrent = domainMatches(domain, currentDomain);
const statusClass = (isCurrent && enabled) ? 'status-active' : 'status-inactive';
const indicator = group.querySelector('.status-indicator');
if (indicator) indicator.className = `status-indicator ${statusClass}`;
if (enabled) group.classList.remove('disabled');
else group.classList.add('disabled');
const toggleBtn = group.querySelector('.domain-toggle-btn');
if (toggleBtn) {
toggleBtn.textContent = enabled ? '✓ 启用' : '✗ 禁用';
if (enabled) toggleBtn.classList.remove('disabled');
else toggleBtn.classList.add('disabled');
}
updateStatsBar();
} else {
refreshManagePanel();
}
}
}
function isRuleEnabled(ruleStr) {
const data = loadData();
return data.ruleStatus[ruleStr] !== false;
}
function setRuleEnabled(ruleStr, enabled) {
const data = loadData();
if (data.ruleStatus[ruleStr] === enabled) return;
data.ruleStatus[ruleStr] = enabled;
saveData(data);
applyRules();
if (shadowRoot) {
const ruleItem = shadowRoot.querySelector(`.rule-item[data-rule-key="${CSS.escape(ruleStr)}"]`);
if (ruleItem) {
const toggleBtn = ruleItem.querySelector('.rule-toggle-btn');
if (toggleBtn) {
toggleBtn.textContent = enabled ? '✓ 启用' : '✗ 禁用';
if (enabled) {
toggleBtn.classList.remove('disabled');
ruleItem.classList.remove('rule-disabled');
} else {
toggleBtn.classList.add('disabled');
ruleItem.classList.add('rule-disabled');
}
}
}
updateStatsBar();
}
}
function toggleRuleEnabled(ruleStr) {
setRuleEnabled(ruleStr, !isRuleEnabled(ruleStr));
}
function parseRule(ruleString) {
const rule = ruleString.trim();
if (!rule || rule.startsWith('!') || rule.startsWith('#')) return null;
const match = rule.match(/^([^#]+)##(.+)$/);
if (match) {
return { domain: match[1].trim(), selector: match[2].trim(), raw: rule };
}
return null;
}
function domainMatches(ruleDomain, currentDomain) {
if (ruleDomain === '*') return true;
if (ruleDomain.startsWith('*.')) {
const base = ruleDomain.substring(2);
return currentDomain === base || currentDomain.endsWith('.' + base);
}
return currentDomain === ruleDomain;
}
function applyRules() {
const hostname = window.location.hostname;
const data = loadData();
const cssParts = [];
for (const ruleStr of data.rules) {
const rule = parseRule(ruleStr);
if (!rule) continue;
if (data.domainStatus[rule.domain] !== false && data.ruleStatus[ruleStr] !== false && domainMatches(rule.domain, hostname)) {
cssParts.push(`${rule.selector} { display: none !important; }`);
}
}
const css = cssParts.join('\n');
if (!styleElement) {
styleElement = document.getElementById('custom-rules-style');
if (!styleElement) {
styleElement = document.createElement('style');
styleElement.id = 'custom-rules-style';
document.head.appendChild(styleElement);
}
}
if (styleElement.textContent !== css) {
styleElement.textContent = css;
}
}
// ========== UI 辅助 ==========
function escapeHtml(str) {
return str.replace(/[&<>]/g, m => m === '&' ? '&' : m === '<' ? '<' : '>');
}
function showMessage(msg, type = 'success') {
if (shadowRoot) {
const statusDiv = shadowRoot.querySelector('#status-message');
if (statusDiv) {
statusDiv.textContent = msg;
statusDiv.style.color = type === 'error' ? '#f44336' : '#4CAF50';
setTimeout(() => {
if (statusDiv.textContent === msg) statusDiv.textContent = '';
}, 2500);
}
}
}
function copyToClipboard(text) {
return new Promise(resolve => {
const ta = document.createElement('textarea');
ta.value = text;
ta.style.position = 'fixed';
ta.style.left = '-9999px';
document.body.appendChild(ta);
ta.select();
let success = false;
try { success = document.execCommand('copy'); } catch(e) {}
if (!success && navigator.clipboard) {
navigator.clipboard.writeText(text).then(() => success = true).catch(() => {});
}
document.body.removeChild(ta);
resolve(success);
});
}
function downloadTextFile(text, filename) {
const blob = new Blob([text], {type: 'text/plain'});
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
a.click();
setTimeout(() => URL.revokeObjectURL(url), 100);
}
function updateStatsBar() {
if (!shadowRoot) return;
const data = loadData();
const totalRules = data.rules.length;
const domains = new Set();
data.rules.forEach(r => { const p = parseRule(r); if(p) domains.add(p.domain); });
const activeRules = data.rules.filter(r => {
const p = parseRule(r);
return p && isDomainEnabled(p.domain) && isRuleEnabled(r) && domainMatches(p.domain, currentDomain);
}).length;
const statsDiv = shadowRoot.querySelector('.stats');
if (statsDiv) {
statsDiv.innerHTML = `共 ${totalRules} 条规则,${domains.size} 个域名
当前域名: ${currentDomain} (${activeRules}条生效)`;
}
}
// ========== 管理面板渲染(所有分组默认折叠) ==========
function renderManagePanel() {
const data = loadData();
const groups = new Map();
for (const ruleStr of data.rules) {
const rule = parseRule(ruleStr);
if (!rule) continue;
if (!groups.has(rule.domain)) groups.set(rule.domain, []);
groups.get(rule.domain).push({ ruleStr, selector: rule.selector });
}
const sortByCurrent = GM_getValue('sort_by_current_domain', true);
let domains = Array.from(groups.keys());
if (sortByCurrent) {
domains.sort((a, b) => {
const aCur = domainMatches(a, currentDomain);
const bCur = domainMatches(b, currentDomain);
if (aCur && !bCur) return -1;
if (!aCur && bCur) return 1;
return a.localeCompare(b);
});
} else {
domains.sort();
}
if (domains.length === 0) {
return `
📝
暂无规则
点击"添加规则"标签创建第一条规则,或导入现有规则
`;
}
const sortHtml = ``;
let groupsHtml = [];
for (const domain of domains) {
const rules = groups.get(domain);
const isCurrent = domainMatches(domain, currentDomain);
const domainEnabled = isDomainEnabled(domain);
const statusClass = (isCurrent && domainEnabled) ? 'status-active' : 'status-inactive';
const domainGroupClass = `domain-group ${isCurrent ? 'current-domain' : ''} ${!domainEnabled ? 'disabled' : ''}`;
const toggleBtnText = domainEnabled ? '✓ 启用' : '✗ 禁用';
const toggleBtnClass = `domain-toggle-btn ${!domainEnabled ? 'disabled' : ''}`;
let rulesHtml = [];
for (const rule of rules) {
const ruleEnabled = isRuleEnabled(rule.ruleStr);
const ruleToggleText = ruleEnabled ? '✓ 启用' : '✗ 禁用';
const ruleToggleClass = `rule-toggle-btn ${!ruleEnabled ? 'disabled' : ''}`;
const ruleItemClass = `rule-item ${!ruleEnabled ? 'rule-disabled' : ''}`;
rulesHtml.push(`
${escapeHtml(domain)}##
${escapeHtml(rule.selector)}
`);
}
// 注意:rules-container 默认没有 expanded 类,max-height 通过内联样式或 CSS 设置为 0
groupsHtml.push(`
▶
${escapeHtml(domain)}
${isCurrent ? '当前' : ''}
${rules.length} 条规则
${rulesHtml.join('')}
`);
}
return sortHtml + groupsHtml.join('');
}
// 刷新管理面板:完全保留用户手动折叠状态,不会自动展开任何分组
function refreshManagePanel() {
if (!shadowRoot) return;
const container = shadowRoot.querySelector('#tab-content');
if (!container) return;
// 保存当前所有域名的折叠状态(基于 data-domain 属性)
const collapsedDomains = new Set();
const currentGroups = shadowRoot.querySelectorAll('.domain-group');
currentGroups.forEach(group => {
const rulesContainer = group.querySelector('.rules-container');
const domain = group.getAttribute('data-domain');
if (rulesContainer && !rulesContainer.classList.contains('expanded') && domain) {
collapsedDomains.add(domain);
}
});
// 重新渲染面板
container.innerHTML = renderManagePanel();
// 恢复折叠状态:对于在 collapsedDomains 中的域名,强制折叠;其他域名也默认折叠(不展开任何分组)
setTimeout(() => {
const newGroups = shadowRoot.querySelectorAll('.domain-group');
newGroups.forEach(group => {
const domain = group.getAttribute('data-domain');
const rulesContainer = group.querySelector('.rules-container');
const chevron = group.querySelector('.chevron');
if (domain && collapsedDomains.has(domain)) {
// 之前是折叠的,保持折叠
if (rulesContainer) {
rulesContainer.style.maxHeight = '0px';
rulesContainer.classList.remove('expanded');
if (chevron) chevron.classList.remove('down');
}
} else {
// 所有其他情况(新域名或之前展开的)都强制设为折叠,不自动展开任何分组
if (rulesContainer) {
rulesContainer.style.maxHeight = '0px';
rulesContainer.classList.remove('expanded');
if (chevron) chevron.classList.remove('down');
}
}
});
}, 20);
updateStatsBar();
setupEventDelegation();
}
function setupEventDelegation() {
if (!shadowRoot) return;
const content = shadowRoot.querySelector('#tab-content');
if (!content) return;
if (content._delegateListener) content.removeEventListener('click', content._delegateListener);
const clickHandler = (e) => {
const target = e.target;
if (target.classList.contains('domain-toggle-btn')) {
e.stopPropagation();
const domain = target.getAttribute('data-domain-toggle');
if (domain) setDomainEnabled(domain, !isDomainEnabled(domain));
return;
}
if (target.classList.contains('delete-domain')) {
e.stopPropagation();
const domain = target.getAttribute('data-domain');
if (domain && confirm(`确定要删除域名“${domain}”下的所有规则吗?\n此操作不可撤销。`)) {
const newRules = getAllRules().filter(r => {
const p = parseRule(r);
return !p || p.domain !== domain;
});
saveRules(newRules);
showMessage(`已删除域名 ${domain} 的所有规则`);
}
return;
}
if (target.classList.contains('rule-toggle-btn')) {
e.stopPropagation();
const rule = decodeURIComponent(target.getAttribute('data-rule-toggle'));
if (rule) toggleRuleEnabled(rule);
return;
}
if (target.classList.contains('copy-rule')) {
e.stopPropagation();
const rule = decodeURIComponent(target.getAttribute('data-rule'));
if (rule) {
copyToClipboard(rule).then(success => {
showMessage(success ? `已复制:${rule.substring(0, 50)}${rule.length>50?'…':''}` : '复制失败', success ? 'success' : 'error');
});
}
return;
}
if (target.classList.contains('delete-rule')) {
e.stopPropagation();
const rule = decodeURIComponent(target.getAttribute('data-rule'));
if (rule && confirm(`确定要删除规则吗?\n\n${rule}`)) {
const newRules = getAllRules().filter(r => r !== rule);
saveRules(newRules);
showMessage('规则已删除');
}
return;
}
const header = target.closest('.domain-header');
if (header && !target.closest('.domain-actions')) {
const group = header.closest('.domain-group');
if (group) {
const rulesContainer = group.querySelector('.rules-container');
const chevron = header.querySelector('.chevron');
if (rulesContainer) {
if (rulesContainer.classList.contains('expanded')) {
rulesContainer.style.maxHeight = '0px';
rulesContainer.classList.remove('expanded');
if (chevron) chevron.classList.remove('down');
} else {
rulesContainer.style.maxHeight = rulesContainer.scrollHeight + 'px';
rulesContainer.classList.add('expanded');
if (chevron) chevron.classList.add('down');
const onEnd = () => {
if (rulesContainer.classList.contains('expanded')) rulesContainer.style.maxHeight = 'none';
rulesContainer.removeEventListener('transitionend', onEnd);
};
rulesContainer.addEventListener('transitionend', onEnd);
}
}
}
return;
}
if (target.classList.contains('domain-name')) {
e.stopPropagation();
const full = target.getAttribute('data-full-domain') || target.textContent;
alert(`完整域名:${full}`);
return;
}
};
content.addEventListener('click', clickHandler);
content._delegateListener = clickHandler;
const sortSwitch = shadowRoot.querySelector('#sort-switch');
if (sortSwitch && !sortSwitch._changeListener) {
sortSwitch.addEventListener('change', function() {
GM_setValue('sort_by_current_domain', this.checked);
refreshManagePanel();
});
sortSwitch._changeListener = true;
}
}
// ========== 添加规则标签页 ==========
function renderAddTab() {
return ``;
}
function bindAddEvents() {
if (!shadowRoot) return;
const addBtn = shadowRoot.querySelector('#batch-add-btn');
const clearBtn = shadowRoot.querySelector('#batch-clear-btn');
const textarea = shadowRoot.querySelector('#batch-input');
if (addBtn) {
addBtn.onclick = () => {
const input = textarea ? textarea.value : '';
const newRules = input.split('\n')
.map(l => l.trim())
.filter(l => l && !l.startsWith('#') && !l.startsWith('!') && l.includes('##'));
if (newRules.length === 0) {
showMessage('没有找到有效的规则', 'error');
return;
}
const existing = getAllRules();
const unique = newRules.filter(r => !existing.includes(r));
if (unique.length === 0) {
showMessage('所有规则都已存在', 'error');
return;
}
saveRules([...existing, ...unique]);
if (textarea) textarea.value = '';
showMessage(`成功添加 ${unique.length} 条新规则`);
const manageTab = shadowRoot.querySelector('.tab-btn[data-tab="manage"]');
if (manageTab) manageTab.click();
};
}
if (clearBtn) {
clearBtn.onclick = () => {
if (textarea) textarea.value = '';
showMessage('已清空输入框');
};
}
}
// ========== 工具标签页 ==========
function renderToolsTab() {
const rules = getAllRules();
return `
工具
统计信息
总规则数: ${rules.length}
当前域名: ${currentDomain}
生效规则: ${rules.filter(r => { const p = parseRule(r); return p && isDomainEnabled(p.domain) && isRuleEnabled(r) && domainMatches(p.domain, currentDomain); }).length} 条
⚠️ 注意事项
• 导入模式:合并导入(保留现有规则)
• 导出规则可以选择复制或下载
• 建议先导出备份再进行操作
• 域名开关:可临时关闭某个域名的所有规则
• 规则开关:可单独启用/禁用某条规则
`;
}
// ========== 导入导出对话框(完整恢复原始样式) ==========
function showExportDialog() {
const dialog = document.createElement('div');
dialog.className = 'dialog-overlay';
dialog.innerHTML = `
共有 ${getAllRules().length} 条规则
📋
复制到剪贴板
将规则复制到剪贴板,然后可以粘贴到其他地方
`;
shadowRoot.appendChild(dialog);
setTimeout(() => dialog.classList.add('active'), 10);
const closeDialog = () => {
dialog.classList.remove('active');
setTimeout(() => dialog.remove(), 300);
};
dialog.querySelector('#export-dialog-close')?.addEventListener('click', closeDialog);
dialog.querySelector('#export-cancel-btn')?.addEventListener('click', closeDialog);
dialog.querySelector('#export-copy-option')?.addEventListener('click', async () => {
const rules = getAllRules();
if (rules.length === 0) {
showMessage('没有规则可以导出', 'error');
closeDialog();
return;
}
const success = await copyToClipboard(rules.join('\n'));
showMessage(success ? `成功复制 ${rules.length} 条规则` : '复制失败', success ? 'success' : 'error');
closeDialog();
});
dialog.querySelector('#export-download-option')?.addEventListener('click', () => {
const rules = getAllRules();
if (rules.length === 0) {
showMessage('没有规则可以导出', 'error');
closeDialog();
return;
}
const filename = `custom-rules-${new Date().toISOString().slice(0,10)}.txt`;
downloadTextFile(rules.join('\n'), filename);
showMessage(`已下载 ${rules.length} 条规则`);
closeDialog();
});
dialog.addEventListener('click', (e) => { if (e.target === dialog) closeDialog(); });
}
function showImportDialog() {
const dialog = document.createElement('div');
dialog.className = 'dialog-overlay';
dialog.innerHTML = `
`;
shadowRoot.appendChild(dialog);
setTimeout(() => dialog.classList.add('active'), 10);
const closeDialog = () => {
dialog.classList.remove('active');
setTimeout(() => dialog.remove(), 300);
};
dialog.querySelector('#import-dialog-close')?.addEventListener('click', closeDialog);
dialog.querySelector('#import-cancel-btn')?.addEventListener('click', closeDialog);
dialog.querySelector('#import-file-option')?.addEventListener('click', () => {
closeDialog();
const fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.accept = '.txt,.text';
fileInput.style.display = 'none';
fileInput.onchange = (e) => {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (ev) => showImportModeDialog(shadowRoot, ev.target.result, 'file');
reader.readAsText(file);
document.body.removeChild(fileInput);
};
document.body.appendChild(fileInput);
fileInput.click();
});
dialog.querySelector('#import-text-option')?.addEventListener('click', () => {
closeDialog();
showTextImportDialog();
});
dialog.addEventListener('click', (e) => { if (e.target === dialog) closeDialog(); });
}
function showTextImportDialog() {
const dialog = document.createElement('div');
dialog.className = 'dialog-overlay';
dialog.innerHTML = `
在下方文本框中粘贴规则,每行一条规则
`;
shadowRoot.appendChild(dialog);
setTimeout(() => dialog.classList.add('active'), 10);
const closeDialog = () => {
dialog.classList.remove('active');
setTimeout(() => dialog.remove(), 300);
};
dialog.querySelector('#text-import-dialog-close')?.addEventListener('click', closeDialog);
dialog.querySelector('#text-import-cancel-btn')?.addEventListener('click', closeDialog);
dialog.querySelector('#text-import-next-btn')?.addEventListener('click', () => {
const content = dialog.querySelector('#text-import-textarea').value;
if (!content.trim()) {
showMessage('请输入规则文本', 'error');
return;
}
closeDialog();
showImportModeDialog(shadowRoot, content, 'text');
});
dialog.addEventListener('click', (e) => { if (e.target === dialog) closeDialog(); });
}
function showImportModeDialog(shadow, content, sourceType) {
const dialog = document.createElement('div');
dialog.className = 'dialog-overlay';
dialog.innerHTML = `
从${sourceType === 'file' ? '文件' : '文本'}中检测到规则
`;
shadow.appendChild(dialog);
setTimeout(() => dialog.classList.add('active'), 10);
let selectedMode = 'merge';
const mergeOpt = dialog.querySelector('#merge-import-option');
const replaceOpt = dialog.querySelector('#replace-import-option');
mergeOpt.classList.add('selected');
mergeOpt.addEventListener('click', () => {
mergeOpt.classList.add('selected');
replaceOpt.classList.remove('selected');
selectedMode = 'merge';
});
replaceOpt.addEventListener('click', () => {
replaceOpt.classList.add('selected');
mergeOpt.classList.remove('selected');
selectedMode = 'replace';
});
const closeDialog = () => {
dialog.classList.remove('active');
setTimeout(() => dialog.remove(), 300);
};
dialog.querySelector('#import-mode-dialog-close')?.addEventListener('click', closeDialog);
dialog.querySelector('#import-mode-cancel-btn')?.addEventListener('click', closeDialog);
dialog.querySelector('#import-mode-confirm-btn')?.addEventListener('click', () => {
const newRules = content.split('\n')
.map(line => line.trim())
.filter(line => line && !line.startsWith('#') && !line.startsWith('!') && line.includes('##'));
if (newRules.length === 0) {
showMessage('没有找到有效的规则', 'error');
closeDialog();
return;
}
if (selectedMode === 'merge') {
const existing = getAllRules();
const unique = newRules.filter(r => !existing.includes(r));
if (unique.length === 0) {
showMessage('没有新规则可添加', 'error');
closeDialog();
return;
}
saveRules([...existing, ...unique]);
showMessage(`合并导入 ${unique.length} 条新规则,共 ${existing.length + unique.length} 条`);
} else {
replaceAllRules(newRules);
showMessage(`替换导入 ${newRules.length} 条规则`);
}
closeDialog();
const manageTab = shadowRoot?.querySelector('.tab-btn[data-tab="manage"]');
if (manageTab) manageTab.click();
});
dialog.addEventListener('click', (e) => { if (e.target === dialog) closeDialog(); });
}
function bindToolsEvents() {
if (!shadowRoot) return;
const exportCard = shadowRoot.querySelector('#export-card');
const importCard = shadowRoot.querySelector('#import-card');
if (exportCard) exportCard.onclick = () => showExportDialog();
if (importCard) importCard.onclick = () => showImportDialog();
}
// ========== 主UI构建(完整原始样式,包含所有对话框CSS) ==========
function buildUI() {
if (uiVisible) return;
if (!document.body) {
window.addEventListener('DOMContentLoaded', buildUI, { once: true });
return;
}
const oldHost = document.getElementById('custom-rules-host');
if (oldHost) oldHost.remove();
const host = document.createElement('div');
host.id = 'custom-rules-host';
const shadow = host.attachShadow({ mode: 'open' });
shadowRoot = shadow;
// 完整样式(包含原始4.9.1所有样式 + 对话框样式)
const style = document.createElement('style');
style.textContent = `
:host {
all: initial;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
z-index: 2147483647;
opacity: 0;
transition: opacity 0.3s;
}
.ui-container {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%) scale(0.9);
width: 90%;
max-width: 700px;
height: 500px;
max-height: 85vh;
background: white;
border-radius: 12px;
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
border: none !important;
outline: none !important;
z-index: 2147483647;
opacity: 0;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
overflow: hidden;
display: flex;
flex-direction: column;
}
.header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 16px 20px;
position: relative;
flex-shrink: 0;
}
.title {
margin: 0;
font-size: 16px;
font-weight: 600;
}
.close-btn {
position: absolute;
top: 12px;
right: 12px;
background: rgba(255,255,255,0.2);
border: none;
color: white;
width: 28px;
height: 28px;
border-radius: 50%;
cursor: pointer;
font-size: 18px;
line-height: 1;
display: flex;
align-items: center;
justify-content: center;
}
.stats {
margin-top: 8px;
font-size: 12px;
opacity: 0.9;
line-height: 1.4;
}
.tabs {
display: flex;
background: #f8f9fa;
border-bottom: 1px solid #e0e0e0;
flex-shrink: 0;
}
.tab-btn {
flex: 1;
padding: 12px;
border: none;
background: none;
cursor: pointer;
font-size: 13px;
font-weight: 500;
color: #666;
transition: all 0.2s;
}
.tab-btn.active {
color: #667eea;
border-bottom: 2px solid #667eea;
}
.main-content {
flex: 1;
overflow: hidden;
display: flex;
flex-direction: column;
min-height: 0;
}
.content-area {
flex: 1;
overflow-y: auto;
padding: 16px;
position: relative;
}
.footer {
padding: 12px 20px;
border-top: 1px solid #f0f0f0;
background: #fafafa;
display: flex;
justify-content: space-between;
align-items: center;
flex-shrink: 0;
}
.help-btn {
background: none;
border: none;
color: #666;
font-size: 12px;
cursor: pointer;
padding: 6px 12px;
border-radius: 4px;
transition: background-color 0.2s;
}
.help-btn:hover {
background-color: #f0f0f0;
}
.status-message {
font-size: 12px;
color: #666;
max-width: 200px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.domain-group {
margin-bottom: 12px;
border: 1px solid #e0e0e0;
border-radius: 8px;
overflow: hidden;
background: white;
transition: transform 0.2s, box-shadow 0.2s;
}
.domain-group.current-domain {
border-color: #667eea;
box-shadow: 0 2px 8px rgba(102, 126, 234, 0.2);
}
.domain-group.current-domain .domain-header {
background: linear-gradient(135deg, rgba(102, 126, 234, 0.1) 0%, rgba(118, 75, 162, 0.1) 100%);
}
.domain-group.disabled {
opacity: 0.7;
background: #f9f9f9;
}
.domain-group.disabled .rules-container .rule-item {
filter: grayscale(0.2);
opacity: 0.7;
}
.domain-header {
background: #f5f5f5;
padding: 12px 16px;
display: flex;
justify-content: space-between;
align-items: center;
cursor: pointer;
user-select: none;
position: relative;
z-index: 1;
}
.domain-info {
display: flex;
align-items: center;
gap: 8px;
flex: 1;
min-width: 0;
}
.status-indicator {
width: 8px;
height: 8px;
border-radius: 50%;
display: inline-block;
flex-shrink: 0;
}
.status-active {
background-color: #4CAF50;
box-shadow: 0 0 0 2px rgba(76, 175, 80, 0.2);
}
.status-inactive {
background-color: #ccc;
}
.domain-content {
flex: 1;
display: flex;
flex-direction: column;
min-width: 0;
}
.domain-name-row {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 2px;
min-width: 0;
}
.domain-name {
font-weight: 500;
color: #333;
font-size: 14px;
max-width: 150px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
cursor: pointer;
transition: color 0.2s;
}
.domain-name:hover {
color: #667eea;
text-decoration: underline;
}
.current-domain-tag {
background: #667eea;
color: white;
font-size: 10px;
padding: 1px 6px;
border-radius: 10px;
font-weight: bold;
margin-left: 4px;
flex-shrink: 0;
}
.rule-count-row {
display: flex;
align-items: center;
}
.rule-count {
font-size: 12px;
color: #666;
font-weight: normal;
padding-left: 16px;
}
.domain-actions {
display: flex;
gap: 8px;
align-items: center;
flex-shrink: 0;
}
.domain-toggle-btn {
background: #667eea;
color: white;
border: none;
padding: 4px 10px;
border-radius: 16px;
cursor: pointer;
font-size: 11px;
font-weight: 500;
transition: 0.2s;
min-width: 48px;
}
.domain-toggle-btn.disabled {
background: #9e9e9e;
}
.delete-domain {
background: #ff4444;
color: white;
border: none;
padding: 4px 10px;
border-radius: 4px;
cursor: pointer;
font-size: 11px;
font-weight: 500;
}
.rules-container {
background: white;
max-height: 0;
overflow: hidden;
transition: max-height 0.3s ease;
position: relative;
z-index: 0;
}
.rules-container.expanded {
max-height: none;
}
.rule-item {
padding: 12px 16px;
border-bottom: 1px solid #f5f5f5;
display: flex;
justify-content: space-between;
align-items: center;
background: #fafafa;
transition: background 0.2s;
}
.rule-item.rule-disabled {
background: #f5f5f5;
opacity: 0.7;
}
.rule-item:nth-child(odd) {
background: #fff;
}
.rule-item:nth-child(odd).rule-disabled {
background: #f9f9f9;
}
.rule-item:hover {
background: #e6f0ff !important;
}
.rule-info {
flex: 1;
}
.rule-domain {
font-size: 12px;
color: #666;
margin-bottom: 4px;
}
.rule-selector {
font-family: 'Consolas', 'Monaco', monospace;
font-size: 12px;
color: #333;
word-break: break-all;
background: rgba(0, 0, 0, 0.02);
padding: 4px 8px;
border-radius: 4px;
border-left: 2px solid #667eea;
}
.rule-actions {
display: flex;
gap: 6px;
flex-shrink: 0;
align-items: center;
}
.rule-toggle-btn {
background: #4CAF50;
color: white;
border: none;
padding: 4px 8px;
border-radius: 4px;
cursor: pointer;
font-size: 11px;
font-weight: 500;
min-width: 52px;
}
.rule-toggle-btn.disabled {
background: #9e9e9e;
}
.copy-rule {
background: #2196F3;
color: white;
border: none;
padding: 4px 8px;
border-radius: 4px;
cursor: pointer;
font-size: 11px;
font-weight: 500;
}
.delete-rule {
background: #ff9800;
color: white;
border: none;
padding: 4px 8px;
border-radius: 4px;
cursor: pointer;
font-size: 11px;
font-weight: 500;
}
.copy-rule:hover, .delete-rule:hover, .rule-toggle-btn:hover {
opacity: 0.85;
}
.action-btn {
flex: 1;
padding: 12px;
color: white;
border: none;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
}
.batch-textarea {
width: 100%;
height: 200px;
padding: 10px;
border: 1px solid #ddd;
border-radius: 6px;
font-family: 'Consolas', 'Monaco', monospace;
font-size: 12px;
box-sizing: border-box;
margin-bottom: 10px;
resize: vertical;
}
.empty-state {
text-align: center;
padding: 30px 16px;
}
.empty-icon {
font-size: 36px;
color: #ddd;
margin-bottom: 12px;
}
.empty-text {
color: #999;
margin-bottom: 6px;
font-size: 13px;
}
.empty-hint {
color: #aaa;
font-size: 12px;
}
.chevron {
display: inline-block;
transition: transform 0.3s;
font-size: 10px;
color: #666;
flex-shrink: 0;
margin-right: 8px;
}
.chevron.down {
transform: rotate(90deg);
}
.content-area::-webkit-scrollbar {
width: 8px;
}
.content-area::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 4px;
}
.content-area::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 4px;
}
.content-area::-webkit-scrollbar-thumb:hover {
background: #a8a8a8;
}
.sort-options {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 16px;
padding-bottom: 12px;
border-bottom: 1px solid #f0f0f0;
}
.sort-label {
font-size: 13px;
color: #666;
font-weight: 500;
}
.sort-toggle {
display: flex;
align-items: center;
gap: 6px;
cursor: pointer;
user-select: none;
padding: 4px 8px;
border-radius: 4px;
transition: background-color 0.2s;
}
.sort-toggle:hover {
background-color: #f0f0f0;
}
.toggle-switch {
position: relative;
display: inline-block;
width: 44px;
height: 24px;
}
.toggle-slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #ccc;
transition: .4s;
border-radius: 34px;
}
.toggle-slider:before {
position: absolute;
content: "";
height: 18px;
width: 18px;
left: 3px;
bottom: 3px;
background-color: white;
transition: .4s;
border-radius: 50%;
box-shadow: 0 1px 3px rgba(0,0,0,0.2);
}
.toggle-switch input:checked + .toggle-slider {
background-color: #667eea;
}
.toggle-switch input:checked + .toggle-slider:before {
transform: translateX(20px);
}
.toggle-switch input {
display: none;
}
.toggle-text {
font-size: 13px;
color: #333;
font-weight: 500;
}
.tools-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
margin-bottom: 20px;
}
.tool-card {
background: white;
border: 1px solid #e0e0e0;
border-radius: 8px;
padding: 16px;
cursor: pointer;
transition: all 0.2s;
text-align: center;
}
.tool-card:hover {
border-color: #667eea;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
transform: translateY(-2px);
}
.tool-card.export {
border-color: #2196F3;
}
.tool-card.import {
border-color: #4CAF50;
}
.tool-icon {
font-size: 32px;
margin-bottom: 8px;
}
.tool-title {
font-size: 14px;
font-weight: 600;
margin-bottom: 4px;
color: #333;
}
.tool-desc {
font-size: 12px;
color: #666;
line-height: 1.4;
}
/* 对话框样式 */
.dialog-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 2147483648;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
opacity: 0;
transition: opacity 0.3s;
pointer-events: none;
}
.dialog-overlay.active {
opacity: 1;
pointer-events: all;
}
.dialog-container {
background: white;
border-radius: 12px;
width: 100%;
max-width: 500px;
max-height: 90vh;
overflow-y: auto;
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
transform: translateY(20px);
transition: transform 0.3s;
}
.dialog-overlay.active .dialog-container {
transform: translateY(0);
}
.dialog-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 16px 20px;
position: relative;
border-radius: 12px 12px 0 0;
}
.dialog-header.export {
background: #2196F3;
}
.dialog-header.import {
background: #4CAF50;
}
.dialog-title {
margin: 0;
font-size: 16px;
font-weight: 600;
}
.dialog-close {
position: absolute;
top: 12px;
right: 12px;
background: rgba(255,255,255,0.2);
border: none;
color: white;
width: 28px;
height: 28px;
border-radius: 50%;
cursor: pointer;
font-size: 18px;
line-height: 1;
display: flex;
align-items: center;
justify-content: center;
}
.dialog-body {
padding: 20px;
}
.dialog-info {
margin-bottom: 16px;
font-size: 14px;
color: #333;
text-align: center;
}
.export-options, .import-options, .import-mode-options {
display: flex;
flex-direction: column;
gap: 12px;
margin: 20px 0;
}
.export-option, .import-option, .import-mode-option {
display: flex;
align-items: center;
padding: 12px;
background: #f8f9fa;
border: 1px solid #e0e0e0;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s;
}
.export-option:hover, .import-option:hover, .import-mode-option:hover {
background: #e3f2fd;
border-color: #2196F3;
}
.export-option-icon, .import-option-icon, .import-mode-option-icon {
font-size: 24px;
margin-right: 12px;
width: 40px;
text-align: center;
}
.export-option-content, .import-option-content, .import-mode-option-content {
flex: 1;
}
.export-option-title, .import-option-title, .import-mode-option-title {
font-size: 14px;
font-weight: 600;
color: #333;
margin-bottom: 4px;
}
.export-option-desc, .import-option-desc, .import-mode-option-desc {
font-size: 12px;
color: #666;
}
.export-cancel-btn, .import-cancel-btn {
width: 100%;
padding: 12px;
background: #f5f5f5;
color: #666;
border: none;
border-radius: 8px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
margin-top: 10px;
}
.export-cancel-btn:hover, .import-cancel-btn:hover {
background: #e0e0e0;
}
.text-import-area {
margin: 20px 0;
}
.text-import-textarea {
width: 100%;
height: 200px;
padding: 12px;
border: 1px solid #ddd;
border-radius: 8px;
font-family: 'Consolas', 'Monaco', monospace;
font-size: 12px;
line-height: 1.5;
resize: vertical;
box-sizing: border-box;
margin-bottom: 16px;
}
.text-import-textarea:focus {
outline: none;
border-color: #4CAF50;
box-shadow: 0 0 0 2px rgba(76, 175, 80, 0.2);
}
.dialog-buttons {
display: flex;
gap: 10px;
margin-top: 20px;
}
.dialog-button {
flex: 1;
padding: 12px;
border: none;
border-radius: 8px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: background-color 0.2s;
}
.dialog-button.primary {
background: #4CAF50;
color: white;
}
.dialog-button.primary:hover {
background: #388E3C;
}
.dialog-button.secondary {
background: #2196F3;
color: white;
}
.dialog-button.secondary:hover {
background: #0b7dda;
}
.dialog-button.cancel {
background: #f5f5f5;
color: #666;
}
.dialog-button.cancel:hover {
background: #e0e0e0;
}
.import-mode-option.selected {
background: #e3f2fd;
border-color: #2196F3;
}
`;
shadow.appendChild(style);
const overlay = document.createElement('div');
overlay.className = 'overlay';
const ui = document.createElement('div');
ui.className = 'ui-container';
shadow.appendChild(overlay);
shadow.appendChild(ui);
document.body.appendChild(host);
requestAnimationFrame(() => {
overlay.style.opacity = '1';
ui.style.opacity = '1';
ui.style.transform = 'translate(-50%, -50%) scale(1)';
});
overlay.addEventListener('click', () => closeUI());
uiVisible = true;
function closeUI() {
overlay.style.opacity = '0';
ui.style.opacity = '0';
ui.style.transform = 'translate(-50%, -50%) scale(0.9)';
setTimeout(() => {
host.remove();
uiVisible = false;
shadowRoot = null;
}, 300);
}
function renderMain() {
const data = loadData();
const totalRules = data.rules.length;
const domainsSet = new Set();
data.rules.forEach(r => { const p = parseRule(r); if(p) domainsSet.add(p.domain); });
const activeRules = data.rules.filter(r => {
const p = parseRule(r);
return p && isDomainEnabled(p.domain) && isRuleEnabled(r) && domainMatches(p.domain, currentDomain);
}).length;
ui.innerHTML = `
`;
const closeBtn = ui.querySelector('#close-ui');
if (closeBtn) closeBtn.onclick = closeUI;
const tabs = ui.querySelectorAll('.tab-btn');
const contentDiv = ui.querySelector('#tab-content');
const switchTab = (tabId) => {
if (tabId === 'manage') {
contentDiv.innerHTML = renderManagePanel();
setupEventDelegation();
} else if (tabId === 'add') {
contentDiv.innerHTML = renderAddTab();
bindAddEvents();
} else if (tabId === 'tools') {
contentDiv.innerHTML = renderToolsTab();
bindToolsEvents();
}
};
tabs.forEach(btn => {
btn.onclick = () => {
tabs.forEach(b => b.classList.remove('active'));
btn.classList.add('active');
switchTab(btn.getAttribute('data-tab'));
};
});
switchTab('manage');
const helpBtn = ui.querySelector('#help-btn');
if (helpBtn) helpBtn.onclick = () => {
alert(`自定义规则管理器使用说明:
规则格式:
域名##CSS选择器
示例:
1. 隐藏特定元素:
www.123pan.com##div.ant-modal-root:has(.tuia-modal-wrap)
2. 使用通配符:
*.baidu.com##.advertisement
匹配所有 baidu.com 的子域名
3. 常用选择器:
.class-name # 按类名
#element-id # 按ID
div[class*='ad'] # 按属性包含
div:has(.ad) # 按子元素
4. 管理规则:
- 按域名分组显示,每条规则显示为两行(域名行 + 选择器行)
- 点击域名分组头可展开/折叠规则列表
- 绿色圆点表示规则在当前页面生效(且域名开关和规则开关都开启)
- 当前域名分组高亮显示,并带有"当前"标签
- 支持复制单条规则、删除单条规则(删除前确认)、删除整个域名组(删除前确认)
- 域名开关:每个域名右侧有“启用/禁用”按钮,可临时关闭该域名下所有规则
- 🆕 规则开关:每条规则右侧有“启用/禁用”按钮,可单独控制规则是否生效
- 禁用后规则不会生效,规则项会变灰显示
- 域名过长会自动截断显示省略号,点击可弹出完整域名
- 排序选项可让当前域名规则置顶
- 规则项交替背景色,鼠标悬停显示浅蓝色反馈
- 所有域名分组默认折叠,点击头部手动展开,添加规则后不会自动展开任何分组
5. 批量添加:
- 每行一条规则,格式:域名##选择器
6. 导入导出(在"工具"标签页):
- 导出规则:复制到剪贴板或下载为文件
- 导入规则:从文件或文本粘贴,支持合并/替换模式
规则立即生效,无需刷新页面。`);
};
}
renderMain();
}
// ========== 初始化 ==========
if (document.head) {
if (!document.getElementById('custom-rules-style')) {
const style = document.createElement('style');
style.id = 'custom-rules-style';
document.head.appendChild(style);
styleElement = style;
} else {
styleElement = document.getElementById('custom-rules-style');
}
} else {
const observer = new MutationObserver(() => {
if (document.head) {
observer.disconnect();
if (!document.getElementById('custom-rules-style')) {
const style = document.createElement('style');
style.id = 'custom-rules-style';
document.head.appendChild(style);
styleElement = style;
} else {
styleElement = document.getElementById('custom-rules-style');
}
applyRules();
}
});
observer.observe(document.documentElement, { childList: true, subtree: true });
}
applyRules();
let lastUrl = location.href;
const urlObserver = new MutationObserver(() => {
if (location.href !== lastUrl) {
lastUrl = location.href;
currentDomain = location.hostname;
applyRules();
if (uiVisible && shadowRoot) {
updateStatsBar();
const activeTab = shadowRoot.querySelector('.tab-btn.active');
if (activeTab && activeTab.getAttribute('data-tab') === 'manage') {
refreshManagePanel();
}
}
}
});
urlObserver.observe(document, { subtree: true, childList: true });
GM_registerMenuCommand('📋 自定义规则', () => buildUI());
unsafeWindow.customRulesManager = {
getAllRules,
saveRules,
replaceAllRules,
applyRules,
showUI: buildUI,
isDomainEnabled,
setDomainEnabled,
isRuleEnabled,
setRuleEnabled,
toggleRuleEnabled
};
console.log('自定义规则管理器 v5.3 已启动(完全手动折叠,导入导出界面完整恢复,支持规则级禁用)');
})();