Universal Web Liberator

Regain Control: Unlocks RightClick/Selection/CopyPaste/Drag On Any Website, Toggle Status With Bottom-Right Button or Ctrl/Meta+Alt+L or Menu Command.

// ==UserScript==
// @name               Universal Web Liberator
// @name:zh-CN         网页枷锁破除
// @name:zh-TW         網頁枷鎖破除
// @description        Regain Control: Unlocks RightClick/Selection/CopyPaste/Drag On Any Website, Toggle Status With Bottom-Right Button or Ctrl/Meta+Alt+L or Menu Command.
// @description:zh-CN  解除网页右键/选择/复制及拖拽限制 恢复自由交互体验 单击右下角图标或使用 Ctrl/Meta+Alt+L 或油猴菜单切换状态
// @description:zh-TW  解除網頁右鍵/選取/複製及拖曳限制 恢復自由互動體驗 單擊右下角圖標或使用 Ctrl/Meta+Alt+L 或油猴菜單切換狀態
// @version            1.3.0
// @icon               https://raw.githubusercontent.com/MiPoNianYou/UserScripts/refs/heads/main/Icons/UniversalWebLiberatorIcon.svg
// @author             念柚
// @namespace          https://github.com/MiPoNianYou/UserScripts
// @supportURL         https://github.com/MiPoNianYou/UserScripts/issues
// @license            GPL-3.0
// @match              *://*/*
// @grant              GM_getValue
// @grant              GM_setValue
// @grant              GM_addStyle
// @grant              GM_registerMenuCommand
// @grant              GM_unregisterMenuCommand
// @run-at             document-start
// ==/UserScript==

(function () {
  "use strict";

  function debounce(func, wait) {
    let timeout;
    return function executedFunction(...args) {
      const later = () => {
        clearTimeout(timeout);
        func.apply(this, args);
      };
      clearTimeout(timeout);
      timeout = setTimeout(later, wait);
    };
  }

  const localizedStrings = {
    "zh-CN": {
      scriptTitle: "网页枷锁破除",
      stateEnabledText: "脚本已启用 ✅",
      stateDisabledText: "脚本已禁用 ❌",
    },
    "zh-TW": {
      scriptTitle: "網頁枷鎖破除",
      stateEnabledText: "腳本已啟用 ✅",
      stateDisabledText: "腳本已禁用 ❌",
    },
    "en-US": {
      scriptTitle: "Universal Web Liberator",
      stateEnabledText: "Liberator Activated ✅",
      stateDisabledText: "Liberator Deactivated ❌",
    },
  };

  function detectUserLanguage() {
    const languages = navigator.languages || [navigator.language];
    for (const lang of languages) {
      const langLower = lang.toLowerCase();
      if (langLower === "zh-cn") return "zh-CN";
      if (
        langLower === "zh-tw" ||
        langLower === "zh-hk" ||
        langLower === "zh-mo"
      )
        return "zh-TW";
      if (langLower === "en-us") return "en-US";
      if (langLower.startsWith("zh-")) return "zh-CN";
      if (langLower.startsWith("en-")) return "en-US";
    }
    for (const lang of languages) {
      const langLower = lang.toLowerCase();
      if (langLower.startsWith("zh")) return "zh-CN";
      if (langLower.startsWith("en")) return "en-US";
    }
    return "en-US";
  }

  class WebLiberator {
    static EventsToStop = [
      "contextmenu",
      "selectstart",
      "copy",
      "cut",
      "paste",
      "dragstart",
      "drag",
    ];
    static InlineEventPropsToClear = [
      "oncontextmenu",
      "onselectstart",
      "oncopy",
      "oncut",
      "onpaste",
      "ondrag",
      "ondragstart",
      "onmousedown",
      "onselect",
      "onbeforecopy",
      "onbeforecut",
      "onbeforepaste",
    ];
    static ScriptIconUrl =
      "https://raw.githubusercontent.com/MiPoNianYou/UserScripts/refs/heads/main/Icons/UniversalWebLiberatorIcon.svg";
    static NotificationId = "WebLiberatorNotification";
    static MenuButtonId = "WebLiberatorMenuButton";
    static NotificationTimeout = 2500;
    static AnimationDuration = 300;
    static STORAGE_KEY_PREFIX = "webLiberator_state_";
    static DEFAULT_ACTIVE_STATE = false;

    observer = null;
    liberationStyleElement = null;
    menuButtonElement = null;
    isActive = WebLiberator.DEFAULT_ACTIVE_STATE;
    boundStopHandler = null;
    notificationTimer = null;
    removalTimer = null;
    currentOrigin = window.location.origin;
    locale = "en-US";
    strings = {};
    menuCommandId = null;

    constructor() {
      this.locale = detectUserLanguage();
      this.strings = localizedStrings[this.locale] || localizedStrings["en-US"];
      this.boundStopHandler = this.stopImmediatePropagationHandler.bind(this);
    }

    getOriginStorageKey() {
      const origin = String(this.currentOrigin || "").replace(/\/$/, "");
      return `${WebLiberator.STORAGE_KEY_PREFIX}${origin}`;
    }

    loadState() {
      const storageKey = this.getOriginStorageKey();
      const defaultStateString = WebLiberator.DEFAULT_ACTIVE_STATE
        ? "enabled"
        : "disabled";
      let storedValue = defaultStateString;
      try {
        storedValue = GM_getValue(storageKey, defaultStateString);
      } catch (e) {}
      if (storedValue !== "enabled" && storedValue !== "disabled") {
        storedValue = defaultStateString;
      }
      this.isActive = storedValue === "enabled";
      return this.isActive;
    }

    saveState() {
      const storageKey = this.getOriginStorageKey();
      const valueToStore = this.isActive ? "enabled" : "disabled";
      try {
        GM_setValue(storageKey, valueToStore);
      } catch (e) {}
    }

    activate() {
      if (this.isActive) return;
      this.isActive = true;
      this.injectLiberationStyles();
      this.bindGlobalEventHijackers();
      this.processExistingNodes(document.documentElement);
      this.initMutationObserver();
      this.updateMenuStatus();
    }

    deactivate() {
      if (!this.isActive) return;
      this.isActive = false;
      this.removeLiberationStyles();
      this.unbindGlobalEventHijackers();
      this.disconnectMutationObserver();
      this.updateMenuStatus();
    }

    toggle() {
      const wasActive = this.isActive;
      if (wasActive) {
        this.deactivate();
        this.showNotification("stateDisabledText");
      } else {
        this.activate();
        this.showNotification("stateEnabledText");
      }
      this.saveState();
      this.updateMenuCommand();
    }

    injectBaseStyles() {
      const notificationCSS = `
        :root {
          --wl-notify-bg-light: rgba(242, 242, 247, 0.85);
          --wl-notify-text-light: rgba(60, 60, 67, 0.9);
          --wl-notify-title-light: rgba(0, 0, 0, 0.9);
          --wl-notify-bg-dark: rgba(44, 44, 46, 0.85);
          --wl-notify-text-dark: rgba(235, 235, 245, 0.8);
          --wl-notify-title-dark: rgba(255, 255, 255, 0.9);
          --wl-shadow-light: 0 6px 20px rgba(100, 100, 100, 0.12);
          --wl-shadow-dark: 0 6px 20px rgba(0, 0, 0, 0.3);
        }
        #${WebLiberator.NotificationId} {
          position: fixed;
          top: 20px;
          right: -400px;
          width: 310px;
          background-color: var(--wl-notify-bg-dark);
          color: var(--wl-notify-text-dark);
          padding: 14px 18px;
          border-radius: 14px;
          font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
          z-index: 2147483646;
          box-shadow: var(--wl-shadow-dark);
          display: flex;
          align-items: center;
          opacity: 0;
          transition: right ${
            WebLiberator.AnimationDuration
          }ms cubic-bezier(0.32, 0.72, 0, 1),
                      opacity ${
                        WebLiberator.AnimationDuration * 0.8
                      }ms ease-out;
          box-sizing: border-box;
          backdrop-filter: blur(18px) saturate(180%);
          -webkit-backdrop-filter: blur(18px) saturate(180%);
          text-align: left;
          border: 1px solid rgba(255, 255, 255, 0.1);
        }
        #${WebLiberator.NotificationId}.visible {
          right: 20px;
          opacity: 1;
        }
        #${WebLiberator.NotificationId} .wl-icon {
          width: 30px;
          height: 30px;
          margin-right: 14px;
          flex-shrink: 0;
        }
        #${WebLiberator.NotificationId} .wl-content {
          display: flex;
          flex-direction: column;
          flex-grow: 1;
          min-width: 0;
        }
        #${WebLiberator.NotificationId} .wl-title {
          font-size: 15px;
          font-weight: 600;
          margin-bottom: 4px;
          color: var(--wl-notify-title-dark);
          white-space: nowrap;
          overflow: hidden;
          text-overflow: ellipsis;
        }
        #${WebLiberator.NotificationId} .wl-message {
          font-size: 13px;
          line-height: 1.45;
          color: var(--wl-notify-text-dark);
          word-wrap: break-word;
          overflow-wrap: break-word;
        }
        @media (prefers-color-scheme: light) {
          #${WebLiberator.NotificationId} {
            background-color: var(--wl-notify-bg-light);
            color: var(--wl-notify-text-light);
            box-shadow: var(--wl-shadow-light);
            border: 1px solid rgba(60, 60, 67, 0.1);
          }
          #${WebLiberator.NotificationId} .wl-title {
            color: var(--wl-notify-title-light);
          }
          #${WebLiberator.NotificationId} .wl-message {
            color: var(--wl-notify-text-light);
          }
        }
      `;

      const menuCSS = `
        :root {
          --wl-menu-bg-light: rgba(242, 242, 247, 0.8);
          --wl-menu-bg-dark: rgba(44, 44, 46, 0.8);
          --wl-shadow-light: 0 4px 15px rgba(100, 100, 100, 0.1);
          --wl-shadow-dark: 0 4px 15px rgba(0, 0, 0, 0.25);
        }
        #${WebLiberator.MenuButtonId} {
          position: fixed;
          bottom: 25px;
          right: 25px;
          width: 44px;
          height: 44px;
          background-color: var(--wl-menu-bg-dark);
          border-radius: 50%;
          cursor: pointer;
          z-index: 2147483647;
          box-shadow: var(--wl-shadow-dark);
          display: flex;
          align-items: center;
          justify-content: center;
          transition: transform 0.2s cubic-bezier(0.32, 0.72, 0, 1),
                      background-color 0.2s ease,
                      opacity 0.2s ease;
          backdrop-filter: blur(12px) saturate(180%);
          -webkit-backdrop-filter: blur(12px) saturate(180%);
          border: 1px solid rgba(255, 255, 255, 0.08);
          opacity: 0.7;
          user-select: none !important;
          -webkit-user-select: none !important;
          -moz-user-select: none !important;
          -ms-user-select: none !important;
          -webkit-user-drag: none !important;
          user-drag: none !important;
        }
        #${WebLiberator.MenuButtonId}:hover {
          transform: scale(1.08);
          opacity: 1;
        }
        #${WebLiberator.MenuButtonId} img {
          width: 22px;
          height: 22px;
          display: block;
          opacity: 0.9;
          transition: opacity 0.2s ease;
          pointer-events: none;
        }
        @media (prefers-color-scheme: light) {
          #${WebLiberator.MenuButtonId} {
            border: 1px solid rgba(60, 60, 67, 0.15);
            box-shadow: var(--wl-shadow-light);
            background-color: var(--wl-menu-bg-light);
          }
          #${WebLiberator.MenuButtonId} img {
            opacity: 0.8;
          }
        }
      `;

      try {
        GM_addStyle(notificationCSS);
        GM_addStyle(menuCSS);
      } catch (e) {}
    }

    injectLiberationStyles() {
      if (
        this.liberationStyleElement ||
        document.getElementById("web-liberator-styles")
      )
        return;
      const css = `
        *,
        *::before,
        *::after {
          user-select: text !important;
          -webkit-user-select: text !important;
          -moz-user-select: text !important;
          -ms-user-select: text !important;
          cursor: auto !important;
          -webkit-user-drag: auto !important;
          user-drag: auto !important;
          pointer-events: auto !important;
        }
        body {
          cursor: auto !important;
        }
        ::selection {
          background-color: highlight !important;
          color: highlighttext !important;
        }
        ::-moz-selection {
          background-color: highlight !important;
          color: highlighttext !important;
        }
      `;
      this.liberationStyleElement = document.createElement("style");
      this.liberationStyleElement.id = "web-liberator-styles";
      this.liberationStyleElement.textContent = css;
      (document.head || document.documentElement).appendChild(
        this.liberationStyleElement
      );
    }

    removeLiberationStyles() {
      this.liberationStyleElement?.remove();
      this.liberationStyleElement = null;
      document.getElementById("web-liberator-styles")?.remove();
    }

    ensureElementsCreated() {
      if (
        this.menuButtonElement &&
        document.body?.contains(this.menuButtonElement)
      ) {
        this.updateMenuStatus();
        return;
      }
      let existingButton = document.getElementById(WebLiberator.MenuButtonId);
      if (existingButton) {
        this.menuButtonElement = existingButton;
        if (!this.menuButtonElement.dataset.listenerAttached) {
          this.menuButtonElement.addEventListener("click", (e) => {
            e.stopPropagation();
            this.toggle();
          });
          this.menuButtonElement.dataset.listenerAttached = "true";
        }
        this.updateMenuStatus();
        return;
      }
      if (document.body) {
        this.createMenuElements();
      } else {
        document.addEventListener(
          "DOMContentLoaded",
          () => {
            this.ensureElementsCreated();
          },
          { once: true }
        );
      }
    }

    createMenuElements() {
      if (!document.body || document.getElementById(WebLiberator.MenuButtonId))
        return;
      this.menuButtonElement = document.createElement("div");
      this.menuButtonElement.id = WebLiberator.MenuButtonId;
      this.menuButtonElement.title = this.strings.scriptTitle;
      this.menuButtonElement.innerHTML = `<img src="${WebLiberator.ScriptIconUrl}" alt="Icon">`;
      this.menuButtonElement.addEventListener("click", (e) => {
        e.stopPropagation();
        this.toggle();
      });
      this.menuButtonElement.dataset.listenerAttached = "true";
      document.body.appendChild(this.menuButtonElement);
      this.updateMenuStatus();
    }

    updateMenuStatus() {
      const button =
        this.menuButtonElement ||
        document.getElementById(WebLiberator.MenuButtonId);
      if (!button) return;
      if (!this.menuButtonElement) this.menuButtonElement = button;
      const isActive = this.isActive;
      const isLightMode = window.matchMedia?.(
        "(prefers-color-scheme: light)"
      ).matches;
      const iconImg = button.querySelector("img");
      let buttonBgColor,
        iconOpacity,
        buttonOpacity = "0.7";
      if (isActive) {
        buttonBgColor = isLightMode
          ? "rgba(52, 199, 89, 0.8)"
          : "rgba(48, 209, 88, 0.8)";
        iconOpacity = "0.95";
        buttonOpacity = "1";
      } else {
        buttonBgColor = isLightMode
          ? "var(--wl-menu-bg-light)"
          : "var(--wl-menu-bg-dark)";
        iconOpacity = isLightMode ? "0.8" : "0.7";
      }
      button.style.backgroundColor = buttonBgColor;
      button.style.opacity = buttonOpacity;
      if (iconImg) iconImg.style.opacity = iconOpacity;
    }

    showNotification(messageKey) {
      if (this.notificationTimer) clearTimeout(this.notificationTimer);
      if (this.removalTimer) clearTimeout(this.removalTimer);
      this.notificationTimer = null;
      this.removalTimer = null;
      const title = this.strings.scriptTitle;
      const message = this.strings[messageKey] || messageKey;
      const displayNotification = () => {
        let notificationElement = document.getElementById(
          WebLiberator.NotificationId
        );
        if (!notificationElement && document.body) {
          notificationElement = document.createElement("div");
          notificationElement.id = WebLiberator.NotificationId;
          notificationElement.innerHTML =
            `<img src="${WebLiberator.ScriptIconUrl}" alt="Icon" class="wl-icon"><div class="wl-content"><div class="wl-title"></div><div class="wl-message"></div></div>`.trim();
          document.body.appendChild(notificationElement);
        } else if (!notificationElement) return;
        const titleElement = notificationElement.querySelector(".wl-title");
        const messageElement = notificationElement.querySelector(".wl-message");
        if (titleElement) titleElement.textContent = title;
        if (messageElement) messageElement.textContent = message;
        notificationElement.classList.remove("visible");
        void notificationElement.offsetWidth;
        requestAnimationFrame(() => {
          const currentElement = document.getElementById(
            WebLiberator.NotificationId
          );
          if (currentElement) {
            setTimeout(() => {
              if (document.getElementById(WebLiberator.NotificationId)) {
                currentElement.classList.add("visible");
              }
            }, 20);
          }
        });
        this.notificationTimer = setTimeout(() => {
          const currentElement = document.getElementById(
            WebLiberator.NotificationId
          );
          if (currentElement) {
            currentElement.classList.remove("visible");
            this.removalTimer = setTimeout(() => {
              document.getElementById(WebLiberator.NotificationId)?.remove();
              this.notificationTimer = null;
              this.removalTimer = null;
            }, WebLiberator.AnimationDuration);
          } else {
            this.notificationTimer = null;
            this.removalTimer = null;
          }
        }, WebLiberator.NotificationTimeout);
      };
      if (document.readyState === "loading") {
        document.addEventListener("DOMContentLoaded", displayNotification, {
          once: true,
        });
      } else {
        displayNotification();
      }
    }

    stopImmediatePropagationHandler(event) {
      event.stopImmediatePropagation();
    }

    bindGlobalEventHijackers() {
      WebLiberator.EventsToStop.forEach((type) => {
        document.addEventListener(type, this.boundStopHandler, {
          capture: true,
          passive: false,
        });
      });
    }

    unbindGlobalEventHijackers() {
      WebLiberator.EventsToStop.forEach((type) => {
        document.removeEventListener(type, this.boundStopHandler, {
          capture: true,
        });
      });
    }

    processExistingNodes(rootNode) {
      if (!this.isActive || !rootNode) return;
      this.clearHandlersRecursive(rootNode);
    }

    clearSingleElementHandlers(element) {
      if (!element || element.nodeType !== Node.ELEMENT_NODE) return;
      for (const prop of WebLiberator.InlineEventPropsToClear) {
        if (
          prop in element &&
          (typeof element[prop] === "function" || element[prop] !== null)
        ) {
          try {
            element[prop] = null;
          } catch (e) {}
        }
        if (element.hasAttribute(prop)) {
          try {
            element.removeAttribute(prop);
          } catch (e) {}
        }
      }
    }

    clearHandlersRecursive(rootNode) {
      if (!this.isActive || !rootNode) return;
      try {
        if (rootNode.nodeType === Node.ELEMENT_NODE) {
          if (
            rootNode.id !== WebLiberator.MenuButtonId &&
            rootNode.id !== WebLiberator.NotificationId
          ) {
            this.clearSingleElementHandlers(rootNode);
          }
          if (rootNode.shadowRoot?.mode === "open")
            this.clearHandlersRecursive(rootNode.shadowRoot);
        }
        const elements = rootNode.querySelectorAll?.("*");
        if (elements) {
          for (const element of elements) {
            if (
              element.id !== WebLiberator.MenuButtonId &&
              element.id !== WebLiberator.NotificationId &&
              !element.closest(`#${WebLiberator.MenuButtonId}`) &&
              !element.closest(`#${WebLiberator.NotificationId}`)
            ) {
              this.clearSingleElementHandlers(element);
              if (element.shadowRoot?.mode === "open")
                this.clearHandlersRecursive(element.shadowRoot);
            }
          }
        }
      } catch (error) {}
    }

    handleMutation(mutations) {
      if (!this.isActive) return;
      for (const mutation of mutations) {
        if (mutation.type === "childList") {
          for (const node of mutation.addedNodes) {
            if (node.nodeType === Node.ELEMENT_NODE) {
              if (
                node.id !== WebLiberator.MenuButtonId &&
                node.id !== WebLiberator.NotificationId &&
                !node.closest(`#${WebLiberator.MenuButtonId}`) &&
                !node.closest(`#${WebLiberator.NotificationId}`)
              ) {
                this.clearSingleElementHandlers(node);
              }
            }
          }
        }
      }
    }

    initMutationObserver() {
      if (this.observer || !document.documentElement) return;
      const observerOptions = { childList: true, subtree: true };
      this.observer = new MutationObserver(this.handleMutation.bind(this));
      try {
        this.observer.observe(document.documentElement, observerOptions);
      } catch (error) {
        this.observer = null;
      }
    }

    disconnectMutationObserver() {
      if (this.observer) {
        this.observer.disconnect();
        this.observer = null;
      }
    }

    updateMenuCommand() {
      if (this.menuCommandId) {
        try {
          GM_unregisterMenuCommand(this.menuCommandId);
        } catch (e) {}
        this.menuCommandId = null;
      }
      const label = this.isActive
        ? this.strings.stateEnabledText
        : this.strings.stateDisabledText;
      const fallbackLabel = this.isActive
        ? "Liberator Activated ✅"
        : "Liberator Deactivated ❌";
      const commandLabel = label || fallbackLabel;
      try {
        this.menuCommandId = GM_registerMenuCommand(commandLabel, () => {
          this.toggle();
        });
      } catch (e) {
        this.menuCommandId = null;
      }
    }
  }

  if (window.self !== window.top) {
    return;
  }

  try {
    const liberator = new WebLiberator();
    liberator.injectBaseStyles();
    liberator.loadState();
    liberator.updateMenuCommand();

    const debouncedToggle = debounce(() => liberator.toggle(), 200);

    document.addEventListener(
      "keydown",
      (event) => {
        if (
          (event.ctrlKey || event.metaKey) &&
          event.altKey &&
          event.code === "KeyL"
        ) {
          event.preventDefault();
          event.stopPropagation();
          debouncedToggle();
        }
      },
      { capture: true }
    );

    const onDOMContentLoaded = () => {
      liberator.ensureElementsCreated();
      if (liberator.isActive) {
        liberator.activate();
      } else {
        liberator.updateMenuStatus();
      }
    };

    if (document.readyState === "loading") {
      document.addEventListener("DOMContentLoaded", onDOMContentLoaded, {
        once: true,
      });
    } else {
      onDOMContentLoaded();
    }
  } catch (error) {}
})();