Enable Select & Copy — WL/BL + Mode Switch + Hotkey

解除网页复制/选中/右键限制;支持白/黑名单模式及切换、站点级覆盖、菜单管理清单、快捷键总开关与 Alt+Shift+C 切换

Mint 2025.09.06.. Lásd a legutóbbi verzió

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

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

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

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

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

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         Enable Select & Copy — WL/BL + Mode Switch + Hotkey
// @namespace    tm-copy-unlock
// @version      3.0
// @description  解除网页复制/选中/右键限制;支持白/黑名单模式及切换、站点级覆盖、菜单管理清单、快捷键总开关与 Alt+Shift+C 切换
// @match        *://*/*
// @run-at       document-start
// @grant        GM_addStyle
// @grant        GM_registerMenuCommand
// @grant        GM_getValue
// @grant        GM_setValue
// @license MIT
// ==/UserScript==

(function () {
  'use strict';

  // ========== 0) 默认配置(可按需改) ==========
  // 默认白名单(仅在这些域名上启用;支持 *.domain.com)
  const DEFAULT_WHITELIST = [
    '*.zhihu.com',
    '*.jianshu.com',
    '*.csdn.net',
    // 'example.com',
  ];
  // 默认黑名单(黑名单模式下,这些域名禁用)
  const DEFAULT_BLACKLIST = [
    // '*.some-interactive-site.com',
  ];

  // ========== 1) 常量 & 存储 Key ==========
  const STORAGE = {
    MODE: 'copyUnlock.mode', // 'whitelist' | 'blacklist'
    WL: 'copyUnlock.whitelist',
    BL: 'copyUnlock.blacklist',
    SITE_OVERRIDE_PREFIX: 'copyUnlock.site.', // + hostname => true/false/null
    HOTKEY_ENABLED: 'copyUnlock.hotkeyEnabled',
  };

  // ========== 2) 工具函数 ==========
  const host = location.hostname;

  function arr(val, fallback = []) {
    return Array.isArray(val) ? val : fallback;
  }

  function unique(a) {
    return Array.from(new Set(a));
  }

  function save(key, value) {
    GM_setValue(key, value);
  }

  function read(key, fallback) {
    const v = GM_getValue(key);
    return v === undefined ? fallback : v;
  }

  function escRegex(s) {
    return s.replace(/[.+^${}()|[\]\\]/g, '\\$&');
  }

  function globToReg(glob) {
    return new RegExp('^' + glob.split('*').map(escRegex).join('.*') + '$', 'i');
  }

  function matchAny(hostname, globs) {
    const regs = globs.map(globToReg);
    return regs.some(r => r.test(hostname));
  }

  // 将当前站点标准化为通配:三级及以上 => *.domain.tld;否则原样
  function currentHostGlob() {
    const parts = host.split('.');
    if (parts.length >= 3) return '*.' + parts.slice(-2).join('.');
    return host;
  }

  function toast(msg) {
    // 轻提示:避免页面没权限时 alert 阻塞
    try {
      console.log('[CopyUnlock]', msg);
      alert(msg);
    } catch {
      /* eslint-disable no-console */
      console.log('[CopyUnlock]', msg);
    }
  }

  // ========== 3) 读取 / 合并清单 & 模式 ==========
  const persistedWL = arr(read(STORAGE.WL, []));
  const persistedBL = arr(read(STORAGE.BL, []));
  const WL = unique([...(DEFAULT_WHITELIST || []), ...persistedWL]);
  const BL = unique([...(DEFAULT_BLACKLIST || []), ...persistedBL]);

  let mode = read(STORAGE.MODE, 'whitelist'); // 'whitelist' | 'blacklist' (默认白名单)
  if (mode !== 'whitelist' && mode !== 'blacklist') {
    mode = 'whitelist';
    save(STORAGE.MODE, mode);
  }

  // 站点覆盖:true=强制开, false=强制关, null/undefined=按模式
  const SITE_KEY = STORAGE.SITE_OVERRIDE_PREFIX + host;
  let siteOverride = read(SITE_KEY, null);

  // 快捷键总开关
  let hotkeyEnabled = read(STORAGE.HOTKEY_ENABLED, true);

  // 根据模式决定是否启用
  const inWL = matchAny(host, WL);
  const inBL = matchAny(host, BL);
  function calcEffectiveOn() {
    if (siteOverride === true) return true;
    if (siteOverride === false) return false;
    if (mode === 'whitelist') return inWL;
    // 黑名单模式:只要不在黑名单,就启用
    return !inBL;
  }
  let effectiveOn = calcEffectiveOn();

  // ========== 4) 菜单:模式切换、站点覆盖、清单管理、快捷键总开关 ==========
  function toggleMode() {
    mode = (mode === 'whitelist') ? 'blacklist' : 'whitelist';
    save(STORAGE.MODE, mode);
    effectiveOn = calcEffectiveOn();
    toast(`已切换为【${mode === 'whitelist' ? '白名单' : '黑名单'}】模式。\n当前站点(${host})状态:${effectiveOn ? '启用' : '禁用'}`);
  }

  function setSiteOverride(val /* true/false/null */) {
    siteOverride = (val === null) ? null : !!val;
    if (val === null) {
      // 清除覆盖
      save(SITE_KEY, null);
      effectiveOn = calcEffectiveOn();
      toast(`已清除当前站点覆盖设定(${host})。\n按模式:${mode === 'whitelist' ? '白名单' : '黑名单'} → ${effectiveOn ? '启用' : '禁用'}`);
    } else {
      save(SITE_KEY, siteOverride);
      effectiveOn = calcEffectiveOn();
      toast(`已${siteOverride ? '开启' : '关闭'} 当前站点解锁复制(${host})。`);
    }
  }

  function addToList(key, listArr, glob) {
    if (!glob) glob = currentHostGlob();
    listArr.push(glob);
    save(key, unique(listArr));
    toast(`已加入:${glob}\n列表:${key}`);
  }

  function removeFromList(key, listArr, glob) {
    if (!glob) glob = currentHostGlob();
    const idx = listArr.indexOf(glob);
    if (idx >= 0) {
      listArr.splice(idx, 1);
      save(key, unique(listArr));
      toast(`已移除:${glob}\n列表:${key}`);
    } else {
      toast(`未找到:${glob}\n列表:${key}`);
    }
  }

  GM_registerMenuCommand(
    `当前模式:${mode === 'whitelist' ? '白名单' : '黑名单'}(点击切换)`,
    toggleMode,
    { autoClose: true }
  );

  GM_registerMenuCommand(
    `${effectiveOn ? '关闭' : '开启'} 当前站点解锁复制`,
    () => setSiteOverride(!effectiveOn),
    { autoClose: true }
  );

  GM_registerMenuCommand(
    '清除当前站点覆盖(恢复按模式判断)',
    () => setSiteOverride(null),
    { autoClose: true }
  );

  GM_registerMenuCommand(
    '将当前站点加入白名单',
    () => addToList(STORAGE.WL, persistedWL, currentHostGlob()),
    { autoClose: true }
  );

  GM_registerMenuCommand(
    '将当前站点移出白名单',
    () => removeFromList(STORAGE.WL, persistedWL, currentHostGlob()),
    { autoClose: true }
  );

  GM_registerMenuCommand(
    '将当前站点加入黑名单',
    () => addToList(STORAGE.BL, persistedBL, currentHostGlob()),
    { autoClose: true }
  );

  GM_registerMenuCommand(
    '将当前站点移出黑名单',
    () => removeFromList(STORAGE.BL, persistedBL, currentHostGlob()),
    { autoClose: true }
  );

  GM_registerMenuCommand(
    '查看白/黑名单',
    () => {
      const curGlob = currentHostGlob();
      toast(
        `当前站点通配:${curGlob}\n\n白名单:\n${JSON.stringify(unique(WL), null, 2)}\n\n黑名单:\n${JSON.stringify(unique(BL), null, 2)}`
      );
    },
    { autoClose: true }
  );

  GM_registerMenuCommand(
    `${hotkeyEnabled ? '关闭' : '开启'} 快捷键 (Alt+Shift+C)`,
    () => {
      hotkeyEnabled = !hotkeyEnabled;
      save(STORAGE.HOTKEY_ENABLED, hotkeyEnabled);
      toast(`快捷键已${hotkeyEnabled ? '开启' : '关闭'}。`);
    },
    { autoClose: true }
  );

  // 快捷键:Alt+Shift+C → 切换当前站点覆盖(开/关)
  window.addEventListener(
    'keydown',
    e => {
      if (!hotkeyEnabled) return;
      if (e.altKey && e.shiftKey && (e.key === 'c' || e.key === 'C')) {
        e.preventDefault();
        setSiteOverride(!effectiveOn);
      }
    },
    true
  );

  // 如果当前不启用,直接退出(保留菜单)
  if (!effectiveOn) return;

  // ========== 5) 解锁复制逻辑 ==========
  const BLOCKED = new Set(['copy', 'cut', 'contextmenu', 'selectstart', 'dragstart', 'beforecopy', 'keydown']);

  // 允许选中/右键 等
  const css = `
    html, body, * {
      -webkit-user-select: text !important;
      -moz-user-select: text !important;
      -ms-user-select: text !important;
      user-select: text !important;
      -webkit-touch-callout: default !important;
      pointer-events: auto !important;
    }
    *[unselectable="on"] {
      -webkit-user-select: text !important;
      user-select: text !important;
    }
  `;
  if (typeof GM_addStyle === 'function') {
    GM_addStyle(css);
  } else {
    const st = document.createElement('style');
    st.textContent = css;
    document.documentElement.appendChild(st);
  }

  // 捕获阶段拦截站点绑定的“禁复制”监听(不破坏浏览器默认复制)
  const stopper = e => {
    if (e.type === 'keydown') {
      const isMac = navigator.platform.toUpperCase().includes('MAC');
      const meta = isMac ? e.metaKey : e.ctrlKey;
      if (meta && (e.key === 'c' || e.key === 'C')) return; // 放行 Ctrl/⌘+C
    }
    // 不 preventDefault,只阻止站点自己的监听链
    e.stopImmediatePropagation();
  };
  for (const t of BLOCKED) {
    window.addEventListener(t, stopper, true);
    document.addEventListener(t, stopper, true);
  }

  // 清理常见 inline 阻断 + 强制可选
  const inlineAttrs = ['oncopy', 'oncut', 'oncontextmenu', 'onselectstart', 'ondragstart', 'onbeforecopy', 'onkeydown'];

  function cleanNode(el) {
    if (!el || el.nodeType !== 1) return;
    for (const a of inlineAttrs) {
      if (a in el) {
        try { el[a] = null; } catch {}
      }
      if (el.hasAttribute && el.hasAttribute(a)) {
        el.removeAttribute(a);
      }
    }
    if (el.getAttribute && el.getAttribute('unselectable') === 'on') {
      el.removeAttribute('unselectable');
    }
    if (el.style) {
      try {
        el.style.setProperty('user-select', 'text', 'important');
        el.style.setProperty('-webkit-user-select', 'text', 'important');
      } catch {}
    }
  }

  function scanTree(root) {
    cleanNode(root);
    const it = document.createNodeIterator(root, NodeFilter.SHOW_ELEMENT);
    let n;
    while ((n = it.nextNode())) cleanNode(n);
  }

  const mo = new MutationObserver(muts => {
    for (const m of muts) {
      if (m.type === 'attributes') cleanNode(m.target);
      else if (m.addedNodes && m.addedNodes.length) {
        m.addedNodes.forEach(n => { if (n.nodeType === 1) scanTree(n); });
      }
    }
  });

  function start() {
    scanTree(document.documentElement);
    mo.observe(document.documentElement, { subtree: true, childList: true, attributes: true });
  }

  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', start, { once: true });
  } else {
    start();
  }

  // Shadow DOM 支持:覆盖 attachShadow,在 shadow root 内注入相同逻辑
  const _attach = Element.prototype.attachShadow;
  if (_attach) {
    Element.prototype.attachShadow = function (init) {
      const root = _attach.call(this, init);
      try {
        const st = document.createElement('style');
        st.textContent = css;
        root.appendChild(st);

        for (const t of BLOCKED) root.addEventListener(t, stopper, true);

        const mo2 = new MutationObserver(muts => {
          for (const m of muts) {
            if (m.type === 'attributes') cleanNode(m.target);
            else if (m.addedNodes && m.addedNodes.length) {
              m.addedNodes.forEach(n => { if (n.nodeType === 1) scanTree(n); });
            }
          }
        });
        mo2.observe(root, { subtree: true, childList: true, attributes: true });
      } catch {}
      return root;
    };
  }

  // 兜底:周期性把 html/body 拉回可选,防止站点反复写死
  setInterval(() => {
    [document.documentElement, document.body].forEach(n => n && cleanNode(n));
  }, 1500);
})();