// ==UserScript==
// @name 复制授权拦截器
// @namespace https://viayoo.com/
// @version 4.4
// @description 复制必弹窗,2次允许后开始自由复制,3次拒绝永久禁用(变灰,点击灰点恢复变红),点击小红点恢复,红绿切换自由复制。
// @author Aloazny & Deepseek
// @match *://*/*
// @run-at document-start
// @icon data:image/png;base64,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
// @grant GM_registerMenuCommand
// @grant GM_getValue
// @grant GM_setValue
// @license MIT
// ==/UserScript==
(function () {
const KEY = 'copyAuth:v4'; 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, shadowRoot = null, container = null;
const UI_TYPES = ['DEFAULT', 'IOS', 'MIUI', 'EDGE'];
let currentUI = (typeof GM_getValue !== 'undefined' ? GM_getValue('clipboard_ui_style', 'EDGE') : 'EDGE');
let editAbortController = null;
const originalExec = document.execCommand;
const origWriteText = navigator.clipboard?.writeText;
const origWrite = navigator.clipboard?.write;
if (typeof GM_registerMenuCommand !== 'undefined') {
const updateMenu = () => {
GM_registerMenuCommand('剪切板设定 🛠️', () => {
if (shadowRoot?.querySelector('.auth-settings-mask')) return;
ensureShadow();
const mask = document.createElement('div'), panel = document.createElement('div');
mask.className = 'auth-settings-mask';
panel.className = 'auth-settings-panel';
const isIgnored = (typeof GM_getValue !== 'undefined' ? GM_getValue('clipboard_ignore_' + location.host, false) : false);
const css = `
.auth-settings-mask { position:fixed; top:0; left:0; width:100%; height:100%; background:rgba(0,0,0,0.3); backdrop-filter:blur(12px); -webkit-backdrop-filter:blur(12px); z-index:2147483646; display:flex; align-items:center; justify-content:center; animation: settings-fade-in 0.3s ease; }
.auth-settings-panel { position:relative; background:rgba(255,255,255,0.85); border-radius:28px; box-shadow:0 25px 50px -12px rgba(0,0,0,0.25); padding:24px; display:flex; flex-direction:column; gap:12px; min-width:240px; font-family:system-ui,-apple-system,sans-serif; animation: settings-slide-in 0.4s cubic-bezier(0.16, 1, 0.3, 1); border:1px solid rgba(255,255,255,0.4); }
.auth-settings-panel button { border:none; border-radius:14px; padding:16px; cursor:pointer; font-size:14px; transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); display:flex; align-items:center; justify-content:space-between; font-weight:600; }
.auth-settings-panel button:active { transform: scale(0.94); }
.btn-ui { background:#fff; color:#007AFF; box-shadow:0 4px 12px rgba(0,122,255,0.1); }
.btn-status { background:${isIgnored ? '#fff1f0' : '#f6ffed'}; color:${isIgnored ? '#ff4d4f' : '#52c41a'}; box-shadow:0 4px 12px ${isIgnored ? 'rgba(255,77,79,0.1)' : 'rgba(82,196,26,0.1)'}; }
.btn-close { margin-top:6px; background:none; color:#888; border:none; font-size:13px; cursor:pointer; text-align:center; font-weight:500; }
.btn-close:hover { color:#444; }
@media (prefers-color-scheme: dark) {
.auth-settings-panel { background:rgba(30,30,30,0.85); border:1px solid rgba(255,255,255,0.1); box-shadow:0 25px 50px -12px rgba(0,0,0,0.5); }
.btn-ui { background:#2c2c2e; color:#0A84FF; box-shadow:0 4px 12px rgba(0,0,0,0.2); }
.btn-status { background:${isIgnored ? '#2c1515' : '#162312'}; color:${isIgnored ? '#ff6961' : '#30d158'}; box-shadow:0 4px 12px rgba(0,0,0,0.2); }
.btn-close { color:#aaa; }
.btn-close:hover { color:#fff; }
}
@keyframes settings-fade-in { from { opacity:0; } to { opacity:1; } }
@keyframes settings-slide-in { from { opacity:0; transform: translateX(120px) scale(0.9); } to { opacity:1; transform: translateX(0) scale(1); } }
`;
const style = document.createElement('style'); style.textContent = css; shadowRoot.appendChild(style);
const btnUI = document.createElement('button');
btnUI.className = 'btn-ui';
const updateBtnText = () => { btnUI.innerHTML = `界面风格 ${currentUI}`; };
updateBtnText();
btnUI.onclick = (e) => {
e.stopPropagation();
currentUI = UI_TYPES[(UI_TYPES.indexOf(currentUI) + 1) % UI_TYPES.length];
if (typeof GM_setValue !== 'undefined') GM_setValue('clipboard_ui_style', currentUI);
updateBtnText();
};
const btnStatus = document.createElement('button');
btnStatus.className = 'btn-status';
const updateStatusText = (ignored) => { btnStatus.innerHTML = `复制授权 ${ignored ? '已禁用' : '监控中'}`; };
updateStatusText(isIgnored);
btnStatus.onclick = (e) => {
e.stopPropagation();
const currentState = !(typeof GM_getValue !== 'undefined' ? GM_getValue('clipboard_ignore_' + location.host, false) : false);
if (typeof GM_setValue !== 'undefined') GM_setValue('clipboard_ignore_' + location.host, currentState);
updateStatusText(currentState);
const isDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
btnStatus.style.background = currentState ? (isDark ? '#2c1515' : '#fff1f0') : (isDark ? '#162312' : '#f6ffed');
btnStatus.style.color = currentState ? (isDark ? '#ff6961' : '#ff4d4f') : (isDark ? '#30d158' : '#52c41a');
btnStatus.style.boxShadow = isDark ? '0 4px 12px rgba(0,0,0,0.2)' : `0 4px 12px ${currentState ? 'rgba(255,77,79,0.1)' : 'rgba(82,196,26,0.1)'}`;
if (currentState) {
enabled = false;
restoreAllCopyAPIs();
if(dot) { dot.remove(); dot = null; }
hideAllModals();
} else {
enabled = true;
hookAPI();
hookExec();
}
};
const btnEdit = document.createElement('button');
btnEdit.className = 'btn-ui';
const updateEditBtn = () => {
const active = editAbortController !== null;
btnEdit.innerHTML = `编辑模式 ${active ? 'ON' : 'OFF'}`;
btnEdit.style.color = active ? '#FF9500' : '#007AFF';
};
updateEditBtn();
btnEdit.onclick = (e) => {
e.stopPropagation();
if (editAbortController !== null) {
disableContentEdit();
} else {
enableContentEdit();
}
updateEditBtn();
};
const isUnlockActive = (typeof GM_getValue !== 'undefined' ? GM_getValue('clipboard_unlock_' + location.host, true) : true);
const btnUnlock = document.createElement('button');
btnUnlock.className = 'btn-status';
const updateUnlockUI = (active) => {
btnUnlock.innerHTML = `解除复制限制 ${active ? '运行中' : '已禁用'}`;
const isDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
btnUnlock.style.background = active ? (isDark ? '#162312' : '#f6ffed') : (isDark ? '#2c1515' : '#fff1f0');
btnUnlock.style.color = active ? (isDark ? '#30d158' : '#52c41a') : (isDark ? '#ff6961' : '#ff4d4f');
};
updateUnlockUI(isUnlockActive);
btnUnlock.onclick = (e) => {
e.stopPropagation();
const newState = !(typeof GM_getValue !== 'undefined' ? GM_getValue('clipboard_unlock_' + location.host, true) : true);
if (typeof GM_setValue !== 'undefined') GM_setValue('clipboard_unlock_' + location.host, newState);
updateUnlockUI(newState);
toggleUnlock(newState);
};
const close = document.createElement('button');
close.className = 'btn-close'; close.textContent = '保存并关闭';
const cleanup = () => { mask.remove(); style.remove(); };
close.onclick = cleanup;
mask.onclick = (e) => { if(e.target === mask) cleanup(); };
panel.append(btnUI, btnStatus, btnUnlock, btnEdit, close);
mask.appendChild(panel);
shadowRoot.appendChild(mask);
}, { id: 'ui_settings_main' });
};
updateMenu();
}
const ensureShadow = () => {
if (shadowRoot) return;
container = document.createElement('div');
container.id = 'copy-auth-interceptor';
container.style.cssText = 'position:absolute;top:0;left:0;z-index:2147483647;';
document.documentElement.appendChild(container);
shadowRoot = container.attachShadow({ mode: 'closed' });
};
const disableAllCopyAPIs = () => {
const emptyFunc = () => false;
const emptyPromise = () => Promise.reject('复制功能已被永久禁用');
document.execCommand = emptyFunc;
if (navigator.clipboard) {
navigator.clipboard.writeText = emptyPromise;
if (navigator.clipboard.write) navigator.clipboard.write = emptyPromise;
}
};
const restoreAllCopyAPIs = () => {
if (originalExec) document.execCommand = originalExec;
if (origWriteText) navigator.clipboard.writeText = origWriteText;
if (origWrite) navigator.clipboard.write = origWrite;
};
const showToast = (msg) => {
if (window.via && typeof window.via.toast === 'function') {
window.via.toast(msg);
} else {
ensureShadow();
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;pointer-events:none;';
shadowRoot.appendChild(t);
setTimeout(() => t.remove(), 2000);
}
};
const hideAllModals = () => {
if (!shadowRoot) return;
const modals = shadowRoot.querySelectorAll('.auth-modal-bg');
modals.forEach(m => m.remove());
isShowingModal = false;
};
const showConfirm = (msg, txt = '') => {
if (isShowingModal) return Promise.resolve(false);
return new Promise(r => {
isShowingModal = true; ensureShadow();
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.className = 'auth-modal-bg';
const safeTxt = txt.slice(0, 500);
const scrollStyle = 'word-break:break-all;white-space:pre-wrap;overflow-y:auto;';
const commonStyle = document.createElement('style');
if (currentUI === 'IOS') {
modal.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,.3);display:flex;align-items:center;justify-content:center;z-index:2147483647;backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);';
dialog.style.cssText = 'background:rgba(255,255,255,.75);backdrop-filter:blur(25px);-webkit-backdrop-filter:blur(25px);border-radius:13px;width:270px;text-align:center;display:flex;flex-direction:column;box-shadow:0 10px 30px rgba(0,0,0,.15);overflow:hidden;animation:ios-zoom .25s cubic-bezier(0.1, 0.9, 0.2, 1);';
const content = document.createElement('div');
content.style.cssText = 'padding:20px 16px;';
title.style.cssText = 'margin:0;font-size:17px;font-weight:600;color:#000;line-height:1.3;';
pre.style.cssText = 'margin-top:6px;font-size:13px;color:#000;max-height:120px;display:' + (txt?'block':'none') + ';line-height:1.4;' + scrollStyle;
btns.style.cssText = 'display:flex;border-top:0.5px solid rgba(0,0,0,.15);height:44px;';
deny.style.cssText = 'flex:1;background:none;border:none;border-right:0.5px solid rgba(0,0,0,.15);color:#007AFF;font-size:17px;cursor:pointer;';
allow.style.cssText = 'flex:1;background:none;border:none;color:#007AFF;font-size:17px;font-weight:600;cursor:pointer;';
commonStyle.textContent = `@keyframes ios-zoom{from{opacity:0;transform:scale(1.15)}to{opacity:1;transform:scale(1)}} @media (prefers-color-scheme: dark) { .auth-modal-bg { background:rgba(0,0,0,.5); } .auth-modal-bg div:first-child { background:rgba(30,30,30,.75) !important; } h3, .auth-modal-bg div div { color:#fff !important; } .auth-modal-bg div div:last-child { border-top-color:rgba(255,255,255,.1) !important; } button { color:#0A84FF !important; border-right-color:rgba(255,255,255,.1) !important; } }`;
title.textContent = msg; pre.textContent = safeTxt; allow.textContent='允许'; deny.textContent='拒绝';
content.append(title, pre); btns.append(deny, allow); dialog.append(content, btns);
} else if (currentUI === 'MIUI') {
modal.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,.4);display:flex;align-items:flex-end;justify-content:center;z-index:2147483647;padding:20px;box-sizing:border-box;backdrop-filter:blur(4px);';
dialog.style.cssText = 'background:#fff;border-radius:28px;width:100%;max-width:400px;text-align:left;box-shadow:0 10px 30px rgba(0,0,0,.1);display:flex;flex-direction:column;gap:12px;padding:24px;animation:miui-in .3s cubic-bezier(0.2,0.8,0.2,1);';
title.style.cssText = 'margin:0;font-size:20px;font-weight:600;color:#1a1a1a;';
pre.style.cssText = 'margin:4px 0;padding:12px;background:#f2f2f2;border-radius:16px;font-size:14px;color:#666;max-height:150px;display:' + (txt?'block':'none') + ';' + scrollStyle;
btns.style.cssText = 'display:flex;gap:12px;margin-top:8px;';
allow.style.cssText = 'flex:1;height:52px;border:none;border-radius:16px;background:#0078FF;color:#fff;font-size:16px;font-weight:600;cursor:pointer;';
deny.style.cssText = 'flex:1;height:52px;border:none;border-radius:16px;background:#eee;color:#333;font-size:16px;font-weight:600;cursor:pointer;';
commonStyle.textContent = `@keyframes miui-in{from{transform:translateY(100%)}to{transform:translateY(0)}} @media (prefers-color-scheme: dark) { .auth-modal-bg div:first-child { background:#222 !important; } h3 { color:#eee !important; } .auth-modal-bg div div:nth-child(2) { background:#333 !important; color:#bbb !important; } button:last-child { background:#333 !important; color:#aaa !important; } }`;
title.textContent = msg; pre.textContent = safeTxt; allow.textContent='允许'; deny.textContent='拒绝';
dialog.append(title, pre, btns); btns.append(allow, deny);
} else if (currentUI === 'EDGE') {
modal.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,.4);display:flex;align-items:flex-end;justify-content:center;z-index:2147483647;backdrop-filter:blur(5px);padding-bottom:40px;box-sizing:border-box;';
dialog.style.cssText = 'background:#fff;border-radius:24px;width:92%;max-width:350px;padding:28px 24px;display:flex;flex-direction:column;gap:20px;box-shadow:0 12px 40px rgba(0,0,0,0.2);animation:edge-pop 0.3s cubic-bezier(0.16, 1, 0.3, 1);box-sizing:border-box;';
title.style.cssText = 'margin:0;font-size:20px;font-weight:700;color:#1a1a1a;line-height:1.4;text-align:left;';
pre.style.cssText = 'margin:0;padding:14px;background:#f5f5f5;border-radius:10px;font-size:13px;color:#444;max-height:120px;display:' + (txt?'block':'none') + ';line-height:1.5;' + scrollStyle;
btns.style.cssText = 'display:flex;flex-direction:column;gap:12px;';
allow.style.cssText = 'width:100%;height:52px;border:none;border-radius:14px;background:#2F78EE;color:#fff;font-size:16px;font-weight:600;cursor:pointer;';
deny.style.cssText = 'width:100%;height:52px;border:none;background:none;color:#2F78EE;font-size:16px;font-weight:600;cursor:pointer;';
commonStyle.textContent = `@keyframes edge-pop{from{opacity:0;transform:translateY(30px) scale(0.98)}to{opacity:1;transform:translateY(0) scale(1)}} @media (prefers-color-scheme: dark) { .auth-modal-bg div:first-child { background:#1c1c1c !important; border:1px solid #333; } h3 { color:#eee !important; } .auth-modal-bg div div:nth-child(2) { background:#2b2b2b !important; color:#bbb !important; } }`;
title.textContent = msg; pre.textContent = safeTxt; allow.textContent='允许复制'; deny.textContent='拒绝复制';
btns.append(allow, deny); dialog.append(title, pre, btns);
} else {
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;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:16px 0 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:160px;display:' + (txt?'block':'none') + ';line-height:1.4;' + scrollStyle;
btns.style.cssText = 'display:flex;gap:12px;margin:0 20px;padding-bottom:20px;';
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);';
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);';
commonStyle.textContent = `@media (prefers-color-scheme: dark) { .auth-modal-bg div:first-child { background:rgba(40,40,40,.94) !important; border-color:rgba(255,255,255,.1) !important; } h3, .auth-modal-bg div div { color:#eee !important; } .auth-modal-bg div div:nth-child(2) { background:#222 !important; } }`;
title.textContent = msg; pre.textContent = safeTxt; allow.textContent='允许'; deny.textContent='拒绝';
dialog.append(title, pre, btns); btns.append(deny, allow);
}
shadowRoot.appendChild(commonStyle);
const clean = () => { modal.remove(); commonStyle.remove(); isShowingModal = false; };
allow.onclick = () => { clean(); r(true); };
deny.onclick = () => { clean(); r(false); };
modal.onclick = e => e.target===modal && deny.onclick();
modal.appendChild(dialog); shadowRoot.appendChild(modal);
});
};
const updateDot = () => {
if (!dot) return;
dot.style.backgroundColor = freeCopyMode ? '#34C759' : (!enabled ? '#8E8E93' : '#FF3B30');
clearTimeout(hideTimer);
if (!freeCopyMode && enabled) {
hideTimer = setTimeout(() => {
if (dot && !freeCopyMode && enabled) {
dot.remove();
dot = null;
}
}, 3000);
}
};
const showDot = () => {
if (dot) {
clearTimeout(hideTimer);
dot.style.opacity = '.8';
dot.style.transform = 'scale(1)';
updateDot();
return;
}
ensureShadow();
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;';
shadowRoot.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; }
else if (freeCopyMode) { freeCopyMode = false; }
else { enabled = true; denyCount = 0; restoreAllCopyAPIs(); }
} else if (taps === 2) {
freeCopyMode = enabled = true; denyCount = 0; restoreAllCopyAPIs();
}
updateDot(); taps = 0;
}, 300);
};
dot.onclick = handleTap; dot.ontouchstart = e => { e.preventDefault(); handleTap(); };
};
const handleAuth = (txt, successCb, failCb) => {
if (!recordCopy('api')) return failCb?.();
if (freeCopyMode) return successCb();
if (!enabled || denyCount >= MAX_DENY) return failCb?.();
showDot();
showConfirm('允许复制内容?', txt).then(ok => {
if (ok) { successCb(); allowCount++; if (allowCount >= AUTO_FREE_AFTER_ALLOW) freeCopyMode = true; }
else {
denyCount++;
if (denyCount >= MAX_DENY) { enabled = false; hideAllModals(); disableAllCopyAPIs(); }
failCb?.();
}
updateDot();
});
};
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; hideAllModals(); updateDot(); copyRecords = []; disableAllCopyAPIs();
showToast('检测到恶意复制行为,复制功能已被永久禁用');
return false;
}
return true;
};
const hookAPI = () => {
if (origWriteText) navigator.clipboard.writeText = txt => new Promise((res, rej) => {
handleAuth(txt, () => origWriteText.call(navigator.clipboard, txt).then(res).catch(rej), () => rej(new Error('Rejected')));
});
if (origWrite) navigator.clipboard.write = data => new Promise((res, rej) => {
const txt = data[0]?.type === 'text/plain' ? data[0].getData('text/plain') : '';
handleAuth(txt, () => origWrite.call(navigator.clipboard, data).then(res).catch(rej), () => rej(new Error('Rejected')));
});
};
const hookExec = () => {
document.execCommand = (cmd, ui, val) => {
if (cmd === 'copy') {
const sel = window.getSelection().toString();
if (!sel) return originalExec.apply(document, arguments);
handleAuth(sel, () => {
const ta = document.createElement('textarea');
ta.value = sel; ta.style.cssText = 'position:fixed;opacity:0;';
document.body.appendChild(ta); ta.select();
originalExec.call(document, 'copy'); ta.remove();
});
return false;
}
return originalExec.apply(document, arguments);
};
};
const enableContentEdit = () => {
if (editAbortController) return;
editAbortController = new AbortController();
const { signal } = editAbortController;
let t = null;
const start = (e) => {
const el = e.target;
if (!['P', 'SPAN', 'DIV', 'H1', 'H2', 'H3', 'H4', 'H5', 'LI', 'B', 'FONT'].includes(el.tagName)) return;
t = setTimeout(() => {
const parent = el.parentNode;
if (parent) {
Array.from(parent.children).forEach(sibling => {
if (sibling.tagName === el.tagName) {
sibling.style.userSelect = 'text';
sibling.style.webkitUserSelect = 'text';
}
});
}
el.setAttribute('contenteditable', 'true');
el.focus();
const bg = el.style.backgroundColor;
el.style.backgroundColor = 'rgba(255, 255, 0, 0.15)';
const r = document.createRange();
r.selectNodeContents(el);
const sel = window.getSelection();
sel.removeAllRanges();
sel.addRange(r);
el.addEventListener('blur', () => {
el.removeAttribute('contenteditable');
el.style.backgroundColor = bg;
}, { once: true });
}, 800);
};
const stop = () => { if (t) clearTimeout(t) };
document.addEventListener('mousedown', start, { capture: true, signal });
document.addEventListener('mouseup', stop, { capture: true, signal });
document.addEventListener('touchstart', start, { capture: true, signal, passive: true });
document.addEventListener('touchend', stop, { capture: true, signal, passive: true });
};
const disableContentEdit = () => {
if (editAbortController) {
editAbortController.abort();
editAbortController = null;
}
};
const toggleUnlock = (active) => {
if (active) {
if (document.getElementById('unlock-css-base')) return;
const s = document.createElement('style');
s.id = 'unlock-css-base';
s.innerHTML = '*{user-select:text!important;-webkit-user-select:text!important;-moz-user-select:text!important;-ms-user-select:text!important;}';
(document.head || document.documentElement).appendChild(s);
const evs = ['copy', 'cut', 'contextmenu', 'selectstart', 'mousedown', 'beforecopy', 'dragstart'];
evs.forEach(n => {
document.addEventListener(n, e => {
if(enabled || freeCopyMode || editAbortController) e.stopImmediatePropagation();
}, { capture: true, passive: false });
try {
Object.defineProperty(document, 'on' + n, { get: () => null, set: () => {}, configurable: true });
} catch (e) {}
});
const opd = Event.prototype.preventDefault;
Event.prototype.preventDefault = function() {
if (evs.includes(this.type)) return;
return opd.apply(this, arguments);
};
} else {
const css = document.getElementById('unlock-css-base');
if (css) css.remove();
}
};
const setupFrame = iframe => {
try {
const doc = iframe.contentDocument, win = doc?.defaultView, frameOrigExec = doc?.execCommand;
if (!doc || !win || !frameOrigExec) return;
doc.execCommand = (cmd, ui, val) => {
if (cmd === 'copy') {
const sel = win.getSelection().toString();
if (!sel) return frameOrigExec.apply(doc, arguments);
handleAuth(sel, () => {
const ta = doc.createElement('textarea');
ta.value = sel; ta.style.cssText = 'position:fixed;opacity:0;';
doc.body.appendChild(ta); ta.select();
frameOrigExec.call(doc, 'copy'); ta.remove();
});
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 = () => {
const isIgnored = (typeof GM_getValue !== 'undefined' ? GM_getValue('clipboard_ignore_' + location.host, false) : false);
const isUnlockAllowed = (typeof GM_getValue !== 'undefined' ? GM_getValue('clipboard_unlock_' + location.host, true) : true);
if (isUnlockAllowed) toggleUnlock(true);
if (!isIgnored) {
hookAPI();
hookExec();
} else {
enabled = false;
}
};
init();
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));
})();