// ==UserScript==
// @name 剪贴板权限控制
// @description 控制网站的写入剪贴板操作,提供允许/拒绝选项
// @version 1.0
// @author WJ
// @match *://*/*
// @license MIT
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_addStyle
// @grant GM_setClipboard
// @grant GM_registerMenuCommand
// @run-at document-start
// @namespace https://greasyfork.org/users/914996
// @downloadURL none
// ==/UserScript==
(() => {
'use strict';
/* ---------- 数据 ---------- */
let whitelist = GM_getValue('whitelist', []);
let blacklist = GM_getValue('blacklist', []);
const domain = location.hostname;
const save = () => {
GM_setValue('whitelist', whitelist);
GM_setValue('blacklist', blacklist);
};
/* ---------- 工具 ---------- */
const toast = (msg) => {
const t = document.createElement('div');
t.className = 'WJ_toast';
t.textContent = msg;
document.body.append(t);
setTimeout(() => t.remove(), 3000);
};
const addWarning = () => {
const header = document.querySelector('.WJ_modal .WJ_header');
if (!header || header.querySelector('.WJ_warning')) return;
const warning = document.createElement('div');
warning.className = 'WJ_warning';
warning.textContent = '⚠️ 此网站授权期多次尝试写入剪贴板 已拒绝后续写入 ⚠️';
warning.style.color = '#FFD700';
warning.style.fontSize = '16px';
warning.style.marginTop = '8px';
header.appendChild(warning);
};
const handleDecision = (action, onAllow, onDeny) => {
({
allow: () => (toast('允许本次复制'), onAllow?.()),
deny: () => (toast('拒绝本次复制'), onDeny?.()),
'always-allow': () => (!whitelist.includes(domain) && (whitelist.push(domain), save()), toast(`添加白名单 ${domain}`), onAllow?.()),
'always-deny': () => (!blacklist.includes(domain) && (blacklist.push(domain), save()), toast(`添加黑名单 ${domain}`), onDeny?.())
}[action] || (() => {}))();
};
/* ---------- 拦截逻辑 ---------- */
let isModalOpen = false;
const decide = (text, onAllow, onDeny) => {
if (isModalOpen) return addWarning(), onDeny?.();
if (whitelist.includes(domain)) return onAllow();
if (blacklist.includes(domain)) return (toast('已拦截复制'), onDeny?.());
isModalOpen = true;
showModal(text, onAllow, onDeny);
};
/* 1. Clipboard API writeText */
navigator.clipboard.writeText = (text) =>
new Promise((res, rej) =>
decide(text, () => { GM_setClipboard(text); res();
}, () => rej(new Error('User denied')))
);
/* 2. Clipboard API write (富文本) */
navigator.clipboard.write = d => new Promise((r, j) => {
const fallback = () => decide('富文本内容?图片/表格 无法显示', () => (GM_setClipboard(''), r()), j);
(d[0]?.types?.includes('text/plain') ? d[0].getType('text/plain')
.then(b => new Response(b).text()) : Promise.reject())
.then(t => decide(t, () => (GM_setClipboard(t), r()), j))
.catch(fallback);
});
/* 3. copy 事件 */
const rawAdd = EventTarget.prototype.addEventListener;
EventTarget.prototype.addEventListener = function(t, l, o) {
if (t === 'copy' && this === document) return;
if (!o && ['touchstart','touchmove','wheel'].includes(t)) o = { passive: true };
rawAdd.call(this, t, l, o);
};
const onCopy = (e) => {
e?.preventDefault?.();
e?.stopImmediatePropagation?.();
const text = getSelection().toString();
decide(text, () => GM_setClipboard(text));
return true;
};
/* 4. execCommand('copy') */
document.oncopy = onCopy;
document.execCommand = function (c, u, v) {
if (c.toLowerCase() === 'copy') {
onCopy({});
return true;
}
return Document.prototype.execCommand.call(this, c, u, v);
};
/* ---------- 弹窗 ---------- */
const showModal = (text, onAllow, onDeny) => {
const overlay = document.createElement('div');
overlay.className = 'WJ_overlay';
const modal = document.createElement('div');
modal.className = 'WJ_modal';
modal.innerHTML = `
${text}
`;
document.documentElement.append(overlay, modal);
const close = () => (overlay.remove(), modal.remove(), isModalOpen = false);
const clickHandler = e => {
const a = e.target.dataset.action;
if (a) handleDecision(a, () => (close(), onAllow?.()), () => (close(), onDeny?.()));
};
modal.onclick = clickHandler;
overlay.onclick = () => (handleDecision('deny', null, onDeny), close());
};
/* ---------- 管理面板 ---------- */
const showPanel = () => {
const overlay = document.createElement('div');
overlay.className = 'WJ_overlay';
const panel = document.createElement('div');
panel.className = 'WJ_modal';
panel.innerHTML = `
白名单
${whitelist.map(d => `
${d}
`).join('') || '
白名单为空
'}
黑名单
${blacklist.map(d => `
${d}
`).join('') || '
黑名单为空
'}
`;
document.documentElement.append(overlay, panel);
panel.addEventListener('click', e => {
if (e.target.id === 'WJ_close') return overlay.remove(), panel.remove();
if (e.target.classList.contains('WJ_delete')) {
const { list, domain } = e.target.dataset;
list === 'whitelist'
? whitelist = whitelist.filter(d => d !== domain)
: blacklist = blacklist.filter(d => d !== domain);
save();
overlay.remove();
panel.remove();
showPanel();
}
});
};
/* ---------- 样式 ---------- */
GM_addStyle(`
.WJ_modal,.WJ_overlay+div{position:fixed;top:65%;left:50%;transform:translate(-50%,-50%);width:90%;max-width:560px;background:#121212;z-index:99999;border-radius:12px;box-shadow:0 10px 40px rgba(0,0,0,.3);font-family:'Segoe UI',Tahoma,Geneva,Verdana,sans-serif;overflow:hidden;}
.WJ_header{background:linear-gradient(135deg,#4a6fa5,#3a5a8a);color:#fff;padding:20px;font-size:20px;font-weight:600;text-align:center;letter-spacing:.5px;}
.WJ_text{white-space:pre-wrap;word-break:break-word;background:#1F2021;padding:5px;border-radius:8px;height:220px;overflow-y:auto;font-family:Consolas,monospace;font-size:15px;line-height:1.5;color:#ccc;margin:15px;border:1px solid #333;}
.WJ_footer{display:grid;grid-template-columns:repeat(4,1fr);gap:8px;padding:15px;background:#1F1F1F;}
.WJ_btn{padding:12px 5px;border:none;border-radius:8px;cursor:pointer;font-weight:600;font-size:14px;color:#fff;text-align:center;transition:transform .1s,filter .1s;}
.WJ_btn:active{transform:scale(0.98);}
.WJ_allow{background:linear-gradient(135deg,#4CAF50,#2E7D32);}
.WJ_deny{background:linear-gradient(135deg,#FF9800,#EF6C00);}
.WJ_always-allow{background:linear-gradient(135deg,#2196F3,#1565C0);}
.WJ_always-deny{background:linear-gradient(135deg,#F44336,#C62828);}
.WJ_close{background:#3D5E90;padding:14px 40px;margin:0 auto;}
.WJ_toast{position:fixed;bottom:30px;left:50%;transform:translateX(-50%);background:rgba(0,0,0,.9);color:#fff;padding:16px 32px;border-radius:8px;border:2px solid #CCC;z-index:99999;font-size:16px;font-weight:500;}
.WJ_overlay{position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,.8);z-index:99998;backdrop-filter:blur(8px);}
.WJ_panel-content{display:flex;padding:0;max-height:60vh;}
.WJ_list{flex:1;padding:20px;overflow-y:auto;border-right:1px solid #333;background:#121212;}
.WJ_list:last-child{border-right:none;}
.WJ_list-title{font-weight:600;margin:0 0 15px;padding-bottom:10px;border-bottom:2px solid #4a6fa5;color:#AAA;text-align:center;font-size:18px;}
.WJ_list-item{display:flex;justify-content:space-between;align-items:center;padding:12px 18px;background:#1F2021;margin-bottom:10px;border-radius:8px;border:1px solid #333;}
.WJ_list-item span{overflow:hidden;text-overflow:ellipsis;color:#ddd;}
.WJ_delete{background:linear-gradient(135deg,#dc3545,#c82333);color:#fff;border:none;border-radius:6px;padding:6px 14px;cursor:pointer;font-size:14px;font-weight:500;margin-left:10px;}
.WJ_close-box{padding:20px;text-align:center;border-top:1px solid #333;background:#1F2021;}
.WJ_empty{text-align:center;padding:20px;color:#6c757d;font-style:italic;}`
);
GM_registerMenuCommand('黑白名单管理', showPanel);
})();