// ==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) {}
})();