// ==UserScript== // @name 选择性解除复制限制 // @namespace http://tampermonkey.net/ // @version 2.0.0 // @description 左上角抽屉式面板,鼠标移过去滑出,勾选即解除复制限制,取消即恢复原样 // @author You // @match *://*/* // @grant GM_setValue // @grant GM_getValue // @grant GM_deleteValue // @run-at document-end // @license MIT // ==/UserScript== (function () { 'use strict'; // ========== 配置 ========== const STORAGE_KEY = 'copy_unlock_sites'; // 存储键名 const PANEL_WIDTH = 145; // 面板宽度 const PANEL_HEIGHT = 40; // 面板高度 const TRIGGER_SIZE = 4; // 触发条宽度(露出的部分) const HIDE_DELAY = 500; // 鼠标离开后延迟缩回的时间(ms) // ========== 工具函数 ========== // 获取当前网站的域名(作为唯一标识) function getCurrentSite() { return location.hostname; } // 获取所有已解锁的网站列表 function getUnlockedSites() { try { return GM_getValue(STORAGE_KEY, []); } catch (e) { return []; } } // 保存已解锁的网站列表 function saveUnlockedSites(sites) { try { GM_setValue(STORAGE_KEY, sites); } catch (e) { console.error('保存设置失败:', e); } } // 检查当前网站是否已解锁 function isCurrentSiteUnlocked() { const sites = getUnlockedSites(); return sites.includes(getCurrentSite()); } // 切换当前网站的解锁状态 function toggleCurrentSite(enabled) { const sites = getUnlockedSites(); const site = getCurrentSite(); const index = sites.indexOf(site); if (enabled && index === -1) { sites.push(site); saveUnlockedSites(sites); applyUnlock(); } else if (!enabled && index !== -1) { sites.splice(index, 1); saveUnlockedSites(sites); removeUnlock(); } } // ========== 解除复制限制核心逻辑 ========== // 存储原始事件处理函数(用于恢复) const originalHandlers = { copy: [], cut: [], selectstart: [], contextmenu: [], beforecopy: [], mousedown: [], mouseup: [] }; // 注入的样式元素ID const STYLE_ID = 'copy-unlock-style'; // 应用解锁 function applyUnlock() { // 1. 注入CSS,允许文本选择 if (!document.getElementById(STYLE_ID)) { const style = document.createElement('style'); style.id = STYLE_ID; style.textContent = ` *, *::before, *::after { -webkit-user-select: text !important; -moz-user-select: text !important; -ms-user-select: text !important; user-select: text !important; -webkit-user-drag: auto !important; } /* 针对常见的禁止复制类名 */ .no-copy, .no-select, .unselectable, [unselectable="on"], [class*="unselect"] { -webkit-user-select: text !important; -moz-user-select: text !important; -ms-user-select: text !important; user-select: text !important; } `; document.head.appendChild(style); } // 2. 在捕获阶段拦截并阻止页面的禁止复制事件 const events = ['copy', 'cut', 'selectstart', 'contextmenu', 'beforecopy', 'mousedown', 'mouseup']; events.forEach(eventName => { // 使用捕获阶段,优先于页面脚本执行 document.addEventListener(eventName, stopPropagationAndAllow, true); window.addEventListener(eventName, stopPropagationAndAllow, true); }); // 3. 移除元素上的 oncopy/oncut/onselectstart 等内联事件 removeInlineEventHandlers(); // 4. 移除常见的禁止复制属性 document.querySelectorAll('*').forEach(el => { if (el.hasAttribute('unselectable')) { el.setAttribute('unselectable', 'off'); } if (el.style.webkitUserSelect === 'none' || el.style.userSelect === 'none' || el.style.MozUserSelect === 'none') { el.dataset.origUserSelect = el.style.userSelect; el.style.webkitUserSelect = 'text'; el.style.userSelect = 'text'; el.style.MozUserSelect = 'text'; } }); // 5. 处理动态加载的内容(MutationObserver) observeDynamicContent(); console.log('[复制解锁] 已解除当前网站的复制限制'); } // 事件拦截函数:允许默认行为,阻止页面脚本的拦截 function stopPropagationAndAllow(e) { // 对于 copy/cut 事件,确保能正常复制 if (e.type === 'copy' || e.type === 'cut') { // 不阻止默认行为,让系统复制正常工作 e.stopImmediatePropagation(); return true; } // 对于 selectstart,允许选择 if (e.type === 'selectstart' || e.type === 'beforecopy') { e.stopImmediatePropagation(); return true; } // 对于 contextmenu,允许右键 if (e.type === 'contextmenu') { e.stopImmediatePropagation(); return true; } // mousedown/mouseup 不做全局阻止,避免影响正常交互 return true; } // 移除内联事件处理器 function removeInlineEventHandlers() { const attrs = ['oncopy', 'oncut', 'onselectstart', 'oncontextmenu', 'onbeforecopy', 'onmousedown', 'onmouseup']; document.querySelectorAll('*').forEach(el => { attrs.forEach(attr => { if (el.hasAttribute(attr)) { // 保存原始值以便恢复 if (!el.dataset['orig_' + attr]) { el.dataset['orig_' + attr] = el.getAttribute(attr); } el.removeAttribute(attr); } }); }); } // MutationObserver 监听动态内容 let observer = null; function observeDynamicContent() { if (observer) return; observer = new MutationObserver((mutations) => { mutations.forEach(mutation => { mutation.addedNodes.forEach(node => { if (node.nodeType === 1) { // 元素节点 // 处理新添加元素的禁止复制属性 if (node.style && (node.style.userSelect === 'none' || node.style.webkitUserSelect === 'none')) { node.style.webkitUserSelect = 'text'; node.style.userSelect = 'text'; } if (node.querySelectorAll) { node.querySelectorAll('*').forEach(el => { if (el.style && (el.style.userSelect === 'none' || el.style.webkitUserSelect === 'none')) { el.style.webkitUserSelect = 'text'; el.style.userSelect = 'text'; } }); } } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); } // 移除解锁(恢复原状) function removeUnlock() { // 1. 移除注入的CSS const style = document.getElementById(STYLE_ID); if (style) { style.remove(); } // 2. 移除事件监听 const events = ['copy', 'cut', 'selectstart', 'contextmenu', 'beforecopy', 'mousedown', 'mouseup']; events.forEach(eventName => { document.removeEventListener(eventName, stopPropagationAndAllow, true); window.removeEventListener(eventName, stopPropagationAndAllow, true); }); // 3. 停止 MutationObserver if (observer) { observer.disconnect(); observer = null; } // 4. 恢复内联事件(尽量恢复) restoreInlineEventHandlers(); // 5. 恢复 style 中的 user-select document.querySelectorAll('[data-orig-user-select]').forEach(el => { el.style.userSelect = el.dataset.origUserSelect; el.style.webkitUserSelect = el.dataset.origUserSelect; delete el.dataset.origUserSelect; }); console.log('[复制解锁] 已恢复当前网站的复制限制'); } // 恢复内联事件处理器 function restoreInlineEventHandlers() { const attrs = ['oncopy', 'oncut', 'onselectstart', 'oncontextmenu', 'onbeforecopy', 'onmousedown', 'onmouseup']; document.querySelectorAll('*').forEach(el => { attrs.forEach(attr => { const origAttr = 'orig_' + attr; if (el.dataset[origAttr]) { el.setAttribute(attr, el.dataset[origAttr]); delete el.dataset[origAttr]; } }); }); } // ========== UI 抽屉面板 ========== let panel = null; let checkbox = null; let hideTimer = null; let isPanelOpen = false; // 创建抽屉面板 function createPanel() { // 外层容器(固定在左上角) const wrapper = document.createElement('div'); wrapper.id = 'copy-unlock-wrapper'; wrapper.style.cssText = ` position: fixed; top: 60px; left: 0; z-index: 2147483647; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; user-select: none; `; // 面板主体 panel = document.createElement('div'); panel.id = 'copy-unlock-panel'; panel.style.cssText = ` position: relative; width: ${PANEL_WIDTH}px; background: #ffffff; border: 1px solid #ccc; border-left: none; border-radius: 0 4px 4px 0; transform: translateX(calc(-100% + ${TRIGGER_SIZE}px)); transition: transform 0.25s ease-out; overflow: hidden; `; // 触发条(露在外面的部分) const triggerBar = document.createElement('div'); triggerBar.style.cssText = ` position: absolute; top: 0; right: -${TRIGGER_SIZE}px; width: ${TRIGGER_SIZE}px; height: 100%; background: #bbb; border-radius: 0 2px 2px 0; cursor: pointer; `; // 内容区域 - 只有复选框和文字 const content = document.createElement('div'); content.style.cssText = ` padding: 10px 12px; display: flex; align-items: center; gap: 6px; `; // 复选框 checkbox = document.createElement('input'); checkbox.type = 'checkbox'; checkbox.id = 'copy-unlock-checkbox'; checkbox.style.cssText = ` width: 14px; height: 14px; cursor: pointer; flex-shrink: 0; `; checkbox.checked = isCurrentSiteUnlocked(); // 标签文字 const labelText = document.createElement('span'); labelText.textContent = '解除本站限制'; labelText.style.cssText = ` font-size: 12px; color: #333; cursor: pointer; white-space: nowrap; `; // 点击文字也能切换 labelText.addEventListener('click', () => { checkbox.checked = !checkbox.checked; checkbox.dispatchEvent(new Event('change')); }); content.appendChild(checkbox); content.appendChild(labelText); // 组装 panel.appendChild(triggerBar); panel.appendChild(content); wrapper.appendChild(panel); document.body.appendChild(wrapper); // ===== 抽屉展开/收起逻辑 ===== function openPanel() { clearTimeout(hideTimer); if (!isPanelOpen) { panel.style.transform = 'translateX(0)'; isPanelOpen = true; } } function closePanel() { clearTimeout(hideTimer); hideTimer = setTimeout(() => { if (isPanelOpen) { panel.style.transform = `translateX(calc(-100% + ${TRIGGER_SIZE}px))`; isPanelOpen = false; } }, HIDE_DELAY); } // 鼠标进入整个区域(包括触发条)就展开 wrapper.addEventListener('mouseenter', openPanel); wrapper.addEventListener('mouseleave', closePanel); // 触摸设备:点击触发条切换 let touchOpen = false; triggerBar.addEventListener('click', (e) => { e.stopPropagation(); if (touchOpen) { panel.style.transform = `translateX(calc(-100% + ${TRIGGER_SIZE}px))`; touchOpen = false; isPanelOpen = false; } else { panel.style.transform = 'translateX(0)'; touchOpen = true; isPanelOpen = true; } }); // 点击页面其他地方关闭(触摸设备) document.addEventListener('click', (e) => { if (touchOpen && !wrapper.contains(e.target)) { panel.style.transform = `translateX(calc(-100% + ${TRIGGER_SIZE}px))`; touchOpen = false; isPanelOpen = false; } }); // ===== 复选框事件 ===== checkbox.addEventListener('change', (e) => { const enabled = e.target.checked; toggleCurrentSite(enabled); }); } // ========== 初始化 ========== function init() { // 创建UI面板 createPanel(); // 如果当前网站已解锁,立即应用 if (isCurrentSiteUnlocked()) { // 延迟一点确保页面加载完成 setTimeout(applyUnlock, 500); } } // 等待 DOM 就绪后初始化 if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })();