Greasy Fork

Greasy Fork is available in English.

复制授权拦截器

复制必弹窗,2次允许后开始自由复制,3次拒绝永久禁用(变灰,点击灰点恢复变红),点击小红点恢复,红绿切换自由复制。

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         复制授权拦截器
// @namespace    https://viayoo.com/
// @version      3.28
// @description  复制必弹窗,2次允许后开始自由复制,3次拒绝永久禁用(变灰,点击灰点恢复变红),点击小红点恢复,红绿切换自由复制。
// @author       Aloazny & Deepseek
// @match        *://*/*
// @run-at       document-start
// @icon       data:image/png;base64,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
// @grant        none
// @license      MIT
// ==/UserScript==

(function () {
    const KEY = 'copyAuth:v3'; 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, COPY_TIME_THRESHOLD = 1000, COPY_COUNT_THRESHOLD = 3;
    let copyRecords = [], dot = null, hideTimer = null;
    const originalExec = document.execCommand;
    const origWriteText = navigator.clipboard?.writeText;
    const origWrite = navigator.clipboard?.write;

    const disableAllCopyAPIs = () => {
        const emptyFunc = () => false;
        const emptyPromise = () => Promise.reject('复制功能已被永久禁用');

        Object.defineProperty(document, 'execCommand', {
            value: emptyFunc,
            writable: false,
            configurable: false
        });

        if (navigator.clipboard) {
            Object.defineProperty(navigator.clipboard, 'writeText', {
                value: emptyPromise,
                writable: false,
                configurable: false
            });
            if (navigator.clipboard.write) {
                Object.defineProperty(navigator.clipboard, 'write', {
                    value: emptyPromise,
                    writable: false,
                    configurable: false
                });
            }
        }

        setInterval(() => {
            document.execCommand = emptyFunc;
            if (navigator.clipboard) {
                navigator.clipboard.writeText = emptyPromise;
                navigator.clipboard.write = emptyPromise;
            }
        }, 800);
    };

    const showToast = (msg) => {
        if (window.via && typeof window.via.toast === 'function') {
            window.via.toast(msg);
        } else {
            const t = document.createElement('div');
            t.textContent = msg;
            t.style.cssText = 'position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);background:rgba(0,0,0,.8);color:white;padding:12px 20px;border-radius:8px;z-index:2147483647;font-size:14px;';
            document.body.appendChild(t);
            setTimeout(() => t.remove(), 2000);
        }
    };

    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);padding:16px;box-sizing:border-box;';
            dialog.style.cssText = 'background:rgba(255,255,255,.94);border-radius:18px;width:88vw;max-width:420px;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: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:48px;border:none;border-radius:14px;background:#007AFF;color:#fff;font-size:16px;font-weight:600;cursor:pointer;box-shadow:0 4px 14px rgba(0,122,255,.35);transition:transform .12s;';
            deny.style.cssText = 'flex:1;height:48px;border:none;border-radius:14px;background:#FF3B30;color:#fff;font-size:16px;font-weight:600;cursor:pointer;box-shadow:0 4px 14px rgba(255,59,48,.35);transition:transform .12s;';
            title.textContent = msg; pre.textContent = txt.slice(0,200)+(txt.length>200?'...':''); allow.textContent='允许'; deny.textContent='拒绝';
            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 updateDot = () => { if (!dot) return; dot.style.backgroundColor = freeCopyMode ? '#34C759' : (!enabled ? '#8E8E93' : '#FF3B30'); };

    const showDot = () => {
        if (dot) { clearTimeout(hideTimer); return; }
        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:0;transform:scale(0);border:2px solid #fff;box-shadow:0 2px 8px rgba(0,0,0,.3);transition:all .3s cubic-bezier(0.34,1.56,0.64,1);cursor:pointer;touch-action:none;';
        document.body.appendChild(dot);
        requestAnimationFrame(() => { dot.style.opacity = '.8'; dot.style.transform = 'scale(1)'; });
        updateDot();
        let taps = 0, timer = null;
        const handleTap = () => {
            taps++; clearTimeout(timer);
            timer = setTimeout(() => {
                if (taps === 1) {
                    if (enabled && !freeCopyMode) { freeCopyMode = true; enabled = true; updateDot(); }
                    else if (freeCopyMode) { freeCopyMode = false; updateDot(); }
                    else { enabled = true; denyCount = 0; updateDot(); }
                } else if (taps === 2) { freeCopyMode = enabled = true; denyCount = 0; updateDot(); }
                taps = 0;
            }, 300);
        };
        dot.onclick = handleTap; dot.ontouchstart = e => { e.preventDefault(); handleTap(); };
    };

    const tryAutoFreeOnAllow = () => { allowCount++; if (allowCount >= AUTO_FREE_AFTER_ALLOW && !freeCopyMode) { freeCopyMode = true; enabled = true; if (dot) updateDot(); } };

    const hideDotIfNeeded = () => {
        if (!dot) return; clearTimeout(hideTimer);
        if (freeCopyMode || !enabled) return;
        hideTimer = setTimeout(() => { if (dot) { dot.remove(); dot = null; } }, 3000);
    };

    const recordCopy = (method) => {
        const now = Date.now();
        copyRecords.push({ timestamp: now, method });

        copyRecords = copyRecords.filter(r => now - r.timestamp <= COPY_TIME_THRESHOLD);

        if (copyRecords.length >= COPY_COUNT_THRESHOLD) {
            enabled = false;
            updateDot();
            copyRecords = [];
            disableAllCopyAPIs();
            showToast('检测到恶意复制行为,复制功能已被永久禁用');
            return false;
        }
        return true;
    };

    const hookAPI = () => {
        if (origWriteText) navigator.clipboard.writeText = txt => new Promise((res, rej) => {
            if (!recordCopy('writeText')) return rej(new Error('恶意复制已被阻止'));
            if (freeCopyMode) return origWriteText.call(navigator.clipboard, txt).then(res).catch(rej);
            if (!enabled || denyCount >= MAX_DENY) return rej();
            showDot();
            showConfirm('允许复制内容?', txt).then(ok => {
                if (ok) { origWriteText.call(navigator.clipboard, txt).then(res).catch(rej); tryAutoFreeOnAllow(); }
                else { denyCount++; if (denyCount >= MAX_DENY) { enabled = false; updateDot(); disableAllCopyAPIs(); } rej(new Error('用户拒绝复制')); }
                hideDotIfNeeded();
            });
        });

        if (origWrite) navigator.clipboard.write = data => new Promise((res, rej) => {
            if (!recordCopy('write')) return rej(new Error('恶意复制已被阻止'));
            if (freeCopyMode) return origWrite.call(navigator.clipboard, data).then(res).catch(rej);
            if (!enabled || denyCount >= MAX_DENY) return rej();
            const txt = data[0]?.type === 'text/plain' ? data[0].getData('text/plain') : '';
            showDot();
            showConfirm('允许复制内容?', txt).then(ok => {
                if (ok) { origWrite.call(navigator.clipboard, data).then(res).catch(rej); tryAutoFreeOnAllow(); }
                else { denyCount++; if (denyCount >= MAX_DENY) { enabled = false; updateDot(); disableAllCopyAPIs(); } rej(new Error('用户拒绝复制')); }
                hideDotIfNeeded();
            });
        });
    };

    const hookExec = () => {
        document.execCommand = (cmd, ui, val) => {
            if (cmd === 'copy') {
                if (!recordCopy('execCommand')) return false;
                if (freeCopyMode) return originalExec.call(document, cmd, ui, val);
                if (!enabled || denyCount >= MAX_DENY) return false;
                const sel = window.getSelection().toString();
                if (!sel) return originalExec.apply(document, arguments);
                showDot();
                showConfirm('允许复制内容?', sel).then(ok => {
                    if (ok) { 
                        const ta = document.createElement('textarea'); 
                        ta.value = sel; ta.style = 'position:fixed;opacity:0;'; 
                        document.body.appendChild(ta); ta.select(); 
                        originalExec.call(document, 'copy'); 
                        ta.remove(); 
                        tryAutoFreeOnAllow(); 
                    } else { 
                        denyCount++; 
                        if (denyCount >= MAX_DENY) { enabled = false; updateDot(); disableAllCopyAPIs(); } 
                    }
                    hideDotIfNeeded();
                });
                return false;
            }
            return originalExec.apply(document, arguments);
        };
    };

    const setupFrame = iframe => {
        try {
            const doc = iframe.contentDocument; if (!doc) return;
            const win = doc.defaultView; if (!win) return;
            const frameOrigExec = doc.execCommand;
            doc.execCommand = (cmd, ui, val) => {
                if (cmd === 'copy') {
                    if (!recordCopy('frameExecCommand')) return false;
                    if (freeCopyMode) return frameOrigExec.call(doc, cmd, ui, val);
                    if (!enabled || denyCount >= MAX_DENY) return false;
                    const sel = win.getSelection().toString();
                    if (!sel) return frameOrigExec.apply(doc, arguments);
                    showDot();
                    showConfirm('允许复制内容?', sel).then(ok => {
                        if (ok) {
                            const ta = doc.createElement('textarea');
                            ta.value = sel; ta.style = 'position:fixed;opacity:0;';
                            doc.body.appendChild(ta); ta.select();
                            frameOrigExec.call(doc, 'copy');
                            ta.remove();
                            tryAutoFreeOnAllow();
                        } else {
                            denyCount++;
                            if (denyCount >= MAX_DENY) { enabled = false; updateDot(); disableAllCopyAPIs(); }
                        }
                        hideDotIfNeeded();
                    });
                    return false;
                }
                return frameOrigExec.apply(doc, arguments);
            };
        } 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 init = () => { hookAPI(); hookExec(); };
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', () => setTimeout(init, 0));
    } else {
        setTimeout(init, 0);
    }

    const prot = setInterval(() => {
        if (navigator.clipboard && navigator.clipboard.writeText === origWriteText) hookAPI();
        if (document.execCommand === originalExec) hookExec();
    }, 1000);
    window.addEventListener('beforeunload', () => clearInterval(prot));
})();