// ==UserScript== // @name 复制授权拦截器 // @namespace https://viayoo.com/ // @version 2.4 // @description 复制必弹窗,2次允许后开始自由复制,3次拒绝永久禁用(变灰,点击灰点恢复变红),点击小红点恢复,红绿切换自由复制。 // @author Aloazny & Deepseek // @match *://*/* // @run-at document-start // @grant none // @license MIT // ==/UserScript== (function () { const KEY = 'copyAuth:v2'; if (window[KEY]) return; window[KEY] = true; let denyCount = 0, allowCount = 0, enabled = true, freeCopyMode = false, isShowingModal = false; const MAX_DENY = 3, AUTO_FREE_AFTER_ALLOW = 2, originalExec = document.execCommand, origWriteText = navigator.clipboard?.writeText, origWrite = navigator.clipboard?.write; const isMobile = () => window.innerWidth <= 768 || 'ontouchstart' in window; const getDialogWidth = () => isMobile() ? '88vw' : '420px'; const getFontSize = () => isMobile() ? '15px' : '16px'; const getBtnHeight = () => isMobile() ? '44px' : '48px'; const showConfirm = (msg, txt = '') => { if (isShowingModal) return Promise.resolve(false); return new Promise(r => { isShowingModal = true; const modal = document.createElement('div'), dialog = document.createElement('div'), title = document.createElement('h3'), pre = document.createElement('div'), btns = document.createElement('div'), allow = document.createElement('button'), deny = document.createElement('button'); modal.style.cssText = `position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,.5);display:flex;align-items:center;justify-content:center;z-index:2147483647;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px);padding:16px;box-sizing:border-box;`; dialog.style.cssText = `background:rgba(255,255,255,.94);border-radius:18px;width:${getDialogWidth()};max-width:100%;text-align:center;box-shadow:0 12px 40px rgba(0,0,0,.25);display:flex;flex-direction:column;gap:16px;border:1px solid rgba(0,0,0,.06);overflow:hidden;`; title.style.cssText = `margin:0;padding:0 20px;font-size:${isMobile()?'17px':'18px'};font-weight:700;color:#111;line-height:1.3;`; pre.style.cssText = `margin:0 20px;padding:14px;background:#f8f8f8;border-radius:12px;font-family:monospace;font-size:13px;color:#111;max-height:140px;overflow:auto;word-break:break-all;display:${txt?'block':'none'};line-height:1.4;`; btns.style.cssText = 'display:flex;gap:12px;margin:0 20px;padding-bottom:8px;'; allow.style.cssText = `flex:1;height:${getBtnHeight()};border:none;border-radius:14px;background:#007AFF;color:#fff;font-size:${getFontSize()};font-weight:600;cursor:pointer;box-shadow:0 4px 14px rgba(0,122,255,.35);transition:transform .12s ease,box-shadow .12s ease;display:flex;align-items:center;justify-content:center;`; deny.style.cssText = `flex:1;height:${getBtnHeight()};border:none;border-radius:14px;background:#FF3B30;color:#fff;font-size:${getFontSize()};font-weight:600;cursor:pointer;box-shadow:0 4px 14px rgba(255,59,48,.35);transition:transform .12s ease,box-shadow .12s ease;display:flex;align-items:center;justify-content:center;`; title.textContent = msg; pre.textContent = txt.slice(0,200)+(txt.length>200?'...':''); allow.textContent='允许'; deny.textContent='拒绝'; allow.onmousedown = deny.onmousedown = e => e.preventDefault(); allow.onclick = () => { clean(); r(true); }; deny.onclick = () => { clean(); r(false); }; modal.onclick = e => e.target===modal && deny.onclick(); const clean = () => { modal.remove(); isShowingModal = false; }; ['touchstart','mousedown'].forEach(ev => { allow.addEventListener(ev, () => allow.style.transform = 'translateY(1px)', {passive:true}); deny.addEventListener(ev, () => deny.style.transform = 'translateY(1px)', {passive:true}); document.addEventListener(ev==='touchstart'?'touchend':'mouseup', () => { allow.style.transform = deny.style.transform = ''; }, {once:true, passive:true}); }); dialog.append(title, pre, btns); btns.append(deny, allow); modal.appendChild(dialog); document.body.appendChild(modal); allow.focus(); }); }; const tryAutoFreeOnAllow = () => { allowCount++; if (allowCount >= AUTO_FREE_AFTER_ALLOW && !freeCopyMode) { freeCopyMode = true; enabled = true; updateDot(); } }; const copyText = txt => origWriteText ? origWriteText.call(navigator.clipboard, txt) : new Promise((res, rej) => { const ta = document.createElement('textarea'); ta.value = txt; ta.style = 'position:fixed;opacity:0;'; document.body.append(ta); ta.select(); originalExec.call(document, 'copy') ? (ta.remove(), res()) : (ta.remove(), rej()); }); const block = e => { if (freeCopyMode || !enabled || denyCount >= MAX_DENY) { if (!enabled || denyCount >= MAX_DENY) e.preventDefault(); return; } e.preventDefault(); e.stopImmediatePropagation(); const sel = window.getSelection().toString(); if (!sel) return; showConfirm('允许复制内容?', sel).then(ok => { if (ok) { copyText(sel).catch(() => {}); tryAutoFreeOnAllow(); } else { denyCount++; if (denyCount >= MAX_DENY) { enabled = false; updateDot(); } } }); }; const hookAPI = () => { if (origWriteText) navigator.clipboard.writeText = txt => new Promise((res, rej) => { if (freeCopyMode) return origWriteText.call(this, txt).then(res).catch(rej); if (!enabled || denyCount >= MAX_DENY) return rej(); showConfirm('允许复制内容?', txt).then(ok => { ok ? (origWriteText.call(this, txt).then(res).catch(rej), tryAutoFreeOnAllow()) : (denyCount++, denyCount>=MAX_DENY&&(enabled=false,updateDot()), rej()); }); }); if (origWrite) navigator.clipboard.write = data => new Promise((res, rej) => { if (freeCopyMode) return origWrite.call(this, data).then(res).catch(rej); if (!enabled || denyCount >= MAX_DENY) return rej(); const txt = data[0]?.type === 'text/plain' ? data[0].getData('text/plain') : ''; showConfirm('允许复制内容?', txt).then(ok => { ok ? (origWrite.call(this, data).then(res).catch(rej), tryAutoFreeOnAllow()) : (denyCount++, denyCount>=MAX_DENY&&(enabled=false,updateDot()), rej()); }); }); }; const hookExec = () => { document.execCommand = (cmd, ui, val) => { if (cmd !== 'copy' || freeCopyMode || !enabled || denyCount >= MAX_DENY) return cmd === 'copy' && (!enabled || denyCount >= MAX_DENY) ? false : originalExec.call(this, cmd, ui, val); const sel = window.getSelection().toString(); if (!sel) return originalExec.apply(this, arguments); showConfirm('允许复制内容?', sel).then(ok => { if (ok) { const ta = document.createElement('textarea'); ta.value = sel; ta.style = 'position:fixed;opacity:0;'; document.body.append(ta); ta.select(); originalExec.call(document, 'copy'); ta.remove(); tryAutoFreeOnAllow(); } else { denyCount++; if (denyCount >= MAX_DENY) { enabled = false; updateDot(); } } }); return false; }; }; const setupFrame = iframe => { try { const doc = iframe.contentDocument; if (!doc) return; const win = doc.defaultView; if (!win) return; doc.addEventListener('copy', block, {capture:true,passive:false}); const orig = doc.execCommand; doc.execCommand = (cmd, ui, val) => { if (cmd !== 'copy' || freeCopyMode || !enabled || denyCount >= MAX_DENY) return cmd === 'copy' && (!enabled || denyCount >= MAX_DENY) ? false : orig.call(this, cmd, ui, val); const sel = win.getSelection().toString(); if (!sel) return orig.apply(this, arguments); showConfirm('允许复制内容?', sel).then(ok => { if (ok) { const ta = doc.createElement('textarea'); ta.value = sel; ta.style = 'position:fixed;opacity:0;'; doc.body.append(ta); ta.select(); orig.call(doc, 'copy'); ta.remove(); tryAutoFreeOnAllow(); } else { denyCount++; if (denyCount >= MAX_DENY) { enabled = false; updateDot(); } } }); return false; }; if (win.navigator.clipboard) { const cb = win.navigator.clipboard, ow = cb.writeText; cb.writeText = txt => new Promise((res, rej) => { if (freeCopyMode) return ow.call(this, txt).then(res).catch(rej); if (!enabled || denyCount >= MAX_DENY) return rej(); showConfirm('允许复制内容?', txt).then(ok => ok ? (ow.call(this, txt).then(res).catch(rej), tryAutoFreeOnAllow()) : (denyCount++, denyCount>=MAX_DENY&&(enabled=false,updateDot()), rej())); }); } } catch(e) {} }; const obs = new MutationObserver(m => { for (const r of m) for (const n of r.addedNodes) if (n.nodeType===1 && n.tagName==='IFRAME') setupFrame(n); }); obs.observe(document.documentElement, {childList:true,subtree:true}); document.querySelectorAll('iframe').forEach(setupFrame); const dot = document.createElement('div'); dot.style.cssText = 'position:fixed;bottom:45%;right:10px;width:20px;height:20px;border-radius:10px;background:#FF3B30;z-index:2147483647;opacity:.8;touch-action:none;cursor:pointer;user-select:none;border:2px solid #fff;box-shadow:0 2px 8px rgba(0,0,0,.3);transition:all .2s ease;'; const updateDot = () => dot.style.backgroundColor = freeCopyMode ? '#34C759' : (!enabled ? '#8E8E93' : '#FF3B30'); let moving = false, sx = 0, sy = 0, il = 0, it = 0, last = 0, taps = 0, timer = null; const start = e => { moving = true; const t = e.touches ? e.touches[0] : e; const r = dot.getBoundingClientRect(); sx = t.clientX; sy = t.clientY; il = r.left; it = r.top; dot.style.transform = 'scale(1.3)'; dot.style.opacity = '1'; e.preventDefault(); }; const move = e => { if (!moving) return; const t = e.touches ? e.touches[0] : e; dot.style.left = (il + t.clientX - sx) + 'px'; dot.style.top = (it + t.clientY - sy) + 'px'; dot.style.right = dot.style.bottom = 'auto'; e.preventDefault(); }; const end = e => { const t = e.changedTouches ? e.changedTouches[0] : e; const moved = Math.abs(t.clientX - sx) > 5 || Math.abs(t.clientY - sy) > 5; if (moving && moved) { moving = false; return; } moving = false; const now = Date.now(); if (now - last < 300) taps++; else taps = 1; last = now; if (taps === 1) timer = setTimeout(() => { freeCopyMode = enabled ? !freeCopyMode : (enabled = true, denyCount = 0, false); updateDot(); taps = 0; }, 300); else if (taps === 2) { clearTimeout(timer); freeCopyMode = enabled = true; denyCount = 0; updateDot(); taps = 0; } dot.style.transform = 'scale(1)'; dot.style.opacity = '.8'; }; dot.addEventListener('touchstart', start, {passive:false}); dot.addEventListener('touchmove', move, {passive:false}); dot.addEventListener('touchend', end, {passive:false}); dot.addEventListener('mousedown', start); document.addEventListener('mousemove', move); document.addEventListener('mouseup', () => moving = false); let mc = 0, mct = null; dot.addEventListener('click', e => { if (moving) { moving = false; return; } mc++; if (mc===1) mct=setTimeout(()=>{freeCopyMode=enabled?!freeCopyMode:(enabled=true,denyCount=0,false);updateDot();mc=0;},300); else if(mc===2){clearTimeout(mct);freeCopyMode=enabled=true;denyCount=0;updateDot();mc=0;} e.stopPropagation(); }); const init = () => { document.addEventListener('copy', block, {capture:true,passive:false}); hookAPI(); hookExec(); const add = () => { if (document.body && !document.body.contains(dot)) document.body.appendChild(dot); else requestAnimationFrame(add); }; document.readyState === 'loading' ? document.addEventListener('DOMContentLoaded', add) : add(); }; document.readyState === 'loading' ? document.addEventListener('DOMContentLoaded', () => setTimeout(init,0)) : setTimeout(init,0); const prot = setInterval(() => { if (navigator.clipboard && !navigator.clipboard._proxied) hookAPI(); if (document.execCommand === originalExec) hookExec(); }, 1000); window.addEventListener('beforeunload', () => clearInterval(prot)); })();