// ==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 // @downloadURL https://update.greasyfork.cloud/scripts/532010/%E7%BD%91%E9%A1%B5%E6%9E%B7%E9%94%81%E7%A0%B4%E9%99%A4.user.js // @updateURL https://update.greasyfork.cloud/scripts/532010/%E7%BD%91%E9%A1%B5%E6%9E%B7%E9%94%81%E7%A0%B4%E9%99%A4.meta.js // ==/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 = `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 = `Icon
`.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) {} })();