// ==UserScript== // @name Via风格自定义规则管理器 // @namespace http://tampermonkey.net/ // @version 4.5.0 // @description 类似Via浏览器的自定义规则功能,支持域名分组管理 // @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-end // @license MIT // ==/UserScript== (function() { 'use strict'; // ========== 规则存储和初始化 ========== const STORAGE_KEY = 'custom_rules_v5'; // 初始为空 const DEFAULT_RULES = []; // 获取所有规则 function getAllRules() { const saved = GM_getValue(STORAGE_KEY, JSON.stringify(DEFAULT_RULES)); try { return JSON.parse(saved); } catch (e) { return DEFAULT_RULES; } } // 保存规则 function saveRules(rules) { GM_setValue(STORAGE_KEY, JSON.stringify(rules)); applyRules(); // 重新应用规则 } // ========== 规则解析和应用 ========== 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 baseDomain = ruleDomain.substring(2); return currentDomain === baseDomain || currentDomain.endsWith('.' + baseDomain); } return currentDomain === ruleDomain; } // 应用规则 function applyRules() { const currentDomain = window.location.hostname; const rules = getAllRules(); let css = ''; rules.forEach(ruleStr => { const rule = parseRule(ruleStr); if (!rule) return; if (domainMatches(rule.domain, currentDomain)) { css += `${rule.selector} { display: none !important; }\n`; } }); if (css) { const oldStyle = document.getElementById('custom-rules-style'); if (oldStyle) oldStyle.remove(); const style = document.createElement('style'); style.id = 'custom-rules-style'; style.textContent = css; document.head.appendChild(style); } } // ========== 通用工具函数 ========== // 复制文本到剪贴板 function copyToClipboard(text) { return new Promise((resolve) => { // 创建临时textarea const textArea = document.createElement('textarea'); textArea.value = text; textArea.style.position = 'fixed'; textArea.style.left = '-9999px'; textArea.style.top = '-9999px'; document.body.appendChild(textArea); try { textArea.select(); textArea.setSelectionRange(0, textArea.value.length); // 尝试使用execCommand const success = document.execCommand('copy'); document.body.removeChild(textArea); if (success) { resolve(true); } else { // 尝试使用Clipboard API if (navigator.clipboard && navigator.clipboard.writeText) { navigator.clipboard.writeText(text).then(() => { resolve(true); }).catch(() => { resolve(false); }); } else { resolve(false); } } } catch (err) { document.body.removeChild(textArea); resolve(false); } }); } // 下载文本文件 function downloadTextFile(text, filename = 'custom-rules.txt') { try { const blob = new Blob([text], { type: 'text/plain;charset=utf-8' }); const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = filename; link.style.display = 'none'; document.body.appendChild(link); link.click(); setTimeout(() => { document.body.removeChild(link); URL.revokeObjectURL(url); }, 100); return true; } catch (err) { console.error('下载失败:', err); return false; } } // 显示消息 function showMessage(message, type = 'success') { const host = document.getElementById('custom-rules-host'); if (host && host.shadowRoot) { const status = host.shadowRoot.querySelector('#status-message'); if (status) { status.textContent = message; status.style.color = type === 'error' ? '#f44336' : '#4CAF50'; // 3秒后清空 setTimeout(() => { if (status && status.textContent === message) { status.textContent = ''; } }, 3000); } } } // ========== UI 管理 ========== let uiVisible = false; let currentDomain = window.location.hostname; // 创建UI function createRuleUI() { // 如果UI已存在,先移除 const oldHost = document.getElementById('custom-rules-host'); if (oldHost) { oldHost.remove(); uiVisible = false; } // 创建宿主元素 const host = document.createElement('div'); host.id = 'custom-rules-host'; // 创建Shadow DOM const shadowRoot = host.attachShadow({ mode: 'open' }); // 添加样式 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; max-height: 85vh; background: white; border-radius: 12px; box-shadow: 0 20px 60px rgba(0,0,0,0.3); 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.current-domain .domain-header .domain-name { color: #667eea; font-weight: 600; } .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; } .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; } .domain-name-row { display: flex; align-items: center; gap: 8px; margin-bottom: 2px; } .domain-name { font-weight: 500; color: #333; font-size: 14px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .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; } .delete-domain { background: #ff4444; color: white; border: none; padding: 6px 12px; border-radius: 4px; cursor: pointer; font-size: 12px; font-weight: 500; flex-shrink: 0; margin-left: 12px; height: 28px; } .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; } .rule-item:nth-child(odd) { background: #fff; } .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; } .delete-rule { background: #ff9800; color: white; border: none; padding: 4px 8px; border-radius: 4px; cursor: pointer; font-size: 12px; font-weight: 500; margin-left: 12px; flex-shrink: 0; } .action-buttons { display: flex; gap: 10px; } .action-btn { flex: 1; padding: 12px; color: white; border: none; border-radius: 6px; font-size: 14px; font-weight: 500; cursor: pointer; } .batch-section { margin-top: 20px; } .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; } .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; } /* 简单的导入导出框 */ .simple-import-box { margin-top: 20px; border: 1px solid #e0e0e0; border-radius: 8px; padding: 16px; background: #f9f9f9; } .simple-import-title { font-size: 14px; font-weight: 600; margin-bottom: 10px; color: #333; } .simple-import-textarea { width: 100%; height: 120px; padding: 10px; border: 1px solid #ddd; border-radius: 4px; font-family: 'Consolas', 'Monaco', monospace; font-size: 12px; box-sizing: border-box; margin-bottom: 10px; resize: vertical; } .simple-buttons { display: flex; gap: 10px; margin-top: 15px; } .simple-btn { flex: 1; padding: 10px; border: none; border-radius: 6px; font-size: 14px; font-weight: 500; cursor: pointer; } .simple-btn.primary { background: #4CAF50; color: white; } .simple-btn.secondary { background: #2196F3; color: white; } .simple-btn.cancel { background: #f5f5f5; color: #666; } /* 导出选项样式 */ .export-options { display: flex; flex-direction: column; gap: 12px; margin: 20px 0; } .export-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 { background: #e3f2fd; border-color: #2196F3; } .export-option-icon { font-size: 24px; margin-right: 12px; width: 40px; text-align: center; } .export-option-content { flex: 1; } .export-option-title { font-size: 14px; font-weight: 600; color: #333; margin-bottom: 4px; } .export-option-desc { font-size: 12px; color: #666; } .export-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 { background: #e0e0e0; } /* 导入选项样式 */ .import-options { display: flex; flex-direction: column; gap: 12px; margin: 20px 0; } .import-option { display: flex; align-items: center; padding: 12px; background: #f8f9fa; border: 1px solid #e0e0e0; border-radius: 8px; cursor: pointer; transition: all 0.2s; } .import-option:hover { background: #e8f5e9; border-color: #4CAF50; } .import-option-icon { font-size: 24px; margin-right: 12px; width: 40px; text-align: center; } .import-option-content { flex: 1; } .import-option-title { font-size: 14px; font-weight: 600; color: #333; margin-bottom: 4px; } .import-option-desc { font-size: 12px; color: #666; } .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; } .import-cancel-btn:hover { background: #e0e0e0; } /* 对话框样式 */ .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; } .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-options { display: flex; flex-direction: column; gap: 12px; margin: 20px 0; } .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; } .import-mode-option:hover { background: #f0f8ff; border-color: #2196F3; } .import-mode-option.selected { background: #e3f2fd; border-color: #2196F3; } .import-mode-option-icon { font-size: 24px; margin-right: 12px; width: 40px; text-align: center; } .import-mode-option-content { flex: 1; } .import-mode-option-title { font-size: 14px; font-weight: 600; color: #333; margin-bottom: 4px; } .import-mode-option-desc { font-size: 12px; color: #666; } `; shadowRoot.appendChild(style); // 创建遮罩层 const overlay = document.createElement('div'); overlay.className = 'overlay'; // 创建主界面 const ui = document.createElement('div'); ui.className = 'ui-container'; ui.id = 'custom-rules-ui'; // 渲染界面内容 renderUI(shadowRoot, ui); shadowRoot.appendChild(overlay); shadowRoot.appendChild(ui); // 将宿主元素添加到文档 document.body.appendChild(host); // 显示动画 setTimeout(() => { overlay.style.opacity = '1'; ui.style.opacity = '1'; ui.style.transform = 'translate(-50%, -50%) scale(1)'; }, 10); // 关闭事件 overlay.addEventListener('click', (e) => { if (e.target === overlay) { closeUI(); } }); uiVisible = true; // 返回关闭函数 function closeUI() { overlay.style.opacity = '0'; ui.style.opacity = '0'; ui.style.transform = 'translate(-50%, -50%) scale(0.9)'; setTimeout(() => { if (host.parentNode) { host.parentNode.removeChild(host); } uiVisible = false; }, 300); } // 绑定关闭按钮 const closeBtn = shadowRoot.querySelector('#close-ui'); if (closeBtn) { closeBtn.addEventListener('click', closeUI); } } // 渲染界面内容 function renderUI(shadowRoot, container) { const rules = getAllRules(); const sortByCurrentDomain = GM_getValue('sort_by_current_domain', true); // 按域名分组 const domainGroups = {}; rules.forEach(ruleStr => { const rule = parseRule(ruleStr); if (!rule) return; if (!domainGroups[rule.domain]) { domainGroups[rule.domain] = []; } domainGroups[rule.domain].push(rule); }); // 计算统计信息 const totalRules = rules.length; const totalDomains = Object.keys(domainGroups).length; const activeRules = rules.filter(ruleStr => { const rule = parseRule(ruleStr); return rule && domainMatches(rule.domain, currentDomain); }).length; // 构建HTML container.innerHTML = `