Universal Web Liberator

Disable webpage restrictions on Right-Click / Selection / Copy / Drag | Restore a seamless interactive experience | Tap the dynamic indicator in the bottom-right corner | Use the shortcut Meta/Ctrl+Alt+L | Toggle state via the userscript menu

  1. // ==UserScript==
  2. // @name Universal Web Liberator
  3. // @name:zh-CN 网页枷锁破除
  4. // @name:zh-TW 網頁枷鎖破除
  5. // @description Disable webpage restrictions on Right-Click / Selection / Copy / Drag | Restore a seamless interactive experience | Tap the dynamic indicator in the bottom-right corner | Use the shortcut Meta/Ctrl+Alt+L | Toggle state via the userscript menu
  6. // @description:zh-CN 解除网页右键菜单/选择文本/拷贝粘贴/拖拽内容限制 恢复自由交互体验 轻点右下角灵动指示器 | 使用快捷键 Meta/Ctrl+Alt+L | 油猴菜单切换状态
  7. // @description:zh-TW 解除網頁右鍵選單/選取文字/複製貼上/拖曳內容限制 恢復自由互動體驗 輕點右下角靈動指示器 | 使用快捷鍵 Meta/Ctrl+Alt+L | 油猴選單切換狀態
  8. // @version 1.4.0
  9. // @icon https://raw.githubusercontent.com/MiPoNianYou/UserScripts/refs/heads/main/Icons/UniversalWebLiberatorIcon.svg
  10. // @author 念柚
  11. // @namespace https://github.com/MiPoNianYou/UserScripts
  12. // @supportURL https://github.com/MiPoNianYou/UserScripts/issues
  13. // @license GPL-3.0
  14. // @match *://*/*
  15. // @grant GM_getValue
  16. // @grant GM_setValue
  17. // @grant GM_addStyle
  18. // @grant GM_registerMenuCommand
  19. // @grant GM_unregisterMenuCommand
  20. // @run-at document-start
  21. // ==/UserScript==
  22.  
  23. (function () {
  24. "use strict";
  25.  
  26. const SCRIPT_SETTINGS = {
  27. DEFAULT_ACTIVATION_STATE: false,
  28. UI_FONT_STACK: "-apple-system, BlinkMacSystemFont, system-ui, sans-serif",
  29. ANIMATION_DURATION_MS: 300,
  30. NOTIFICATION_VISIBILITY_DURATION_MS: 2000,
  31. INDICATOR_EXPANDED_DURATION_MS: 2000,
  32. STATE_TOGGLE_DEBOUNCE_MS: 200,
  33. };
  34.  
  35. const RESTRICTION_OVERRIDES = {
  36. EVENTS_TO_INTERCEPT: [
  37. "contextmenu",
  38. "selectstart",
  39. "copy",
  40. "cut",
  41. "paste",
  42. "drag",
  43. "dragstart",
  44. ],
  45. INLINE_HANDLER_ATTRIBUTES_TO_CLEAR: [
  46. "onmousedown",
  47. "oncontextmenu",
  48. "onselect",
  49. "onselectstart",
  50. "oncopy",
  51. "oncut",
  52. "onpaste",
  53. "onbeforecopy",
  54. "onbeforecut",
  55. "onbeforepaste",
  56. "ondrag",
  57. "ondragstart",
  58. ],
  59. };
  60.  
  61. const ELEMENT_IDS = {
  62. STATUS_NOTIFICATION: "WebLiberatorStatusNotification",
  63. EDGE_INDICATOR: "WebLiberatorEdgeIndicator",
  64. OVERRIDE_STYLE_SHEET: "WebLiberatorOverrideStyleSheet",
  65. };
  66.  
  67. const CSS_CLASSES = {
  68. STATUS_NOTIFICATION_VISIBLE: "wl-status-notification--visible",
  69. STATUS_NOTIFICATION_ICON: "wl-status-notification-icon",
  70. STATUS_NOTIFICATION_CONTENT: "wl-status-notification-content",
  71. STATUS_NOTIFICATION_TITLE: "wl-status-notification-title",
  72. STATUS_NOTIFICATION_MESSAGE: "wl-status-notification-message",
  73. EDGE_INDICATOR_EXPANDED: "wl-edge-indicator--expanded",
  74. EDGE_INDICATOR_ACTIVE: "wl-edge-indicator--active",
  75. };
  76.  
  77. const STORAGE_KEYS = {
  78. ACTIVATION_STATE_PREFIX: "webLiberator_state_",
  79. STATE_ENABLED_VALUE: "enabled",
  80. STATE_DISABLED_VALUE: "disabled",
  81. };
  82.  
  83. const UI_TEXTS = {
  84. "zh-CN": {
  85. SCRIPT_TITLE: "网页枷锁破除",
  86. STATUS_ENABLED: "脚本已启用 ✅",
  87. STATUS_DISABLED: "脚本已禁用 ❌",
  88. },
  89. "zh-TW": {
  90. SCRIPT_TITLE: "網頁枷鎖破除",
  91. STATUS_ENABLED: "腳本已啟用 ✅",
  92. STATUS_DISABLED: "腳本已禁用 ❌",
  93. },
  94. "en-US": {
  95. SCRIPT_TITLE: "Universal Web Liberator",
  96. STATUS_ENABLED: "Liberator Activated ✅",
  97. STATUS_DISABLED: "Liberator Deactivated ❌",
  98. },
  99. };
  100.  
  101. const SVG_ICON_MARKUP = `
  102. <svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 125.834 145.398">
  103. <path d="M103.957 43.1334L103.957 102.362C103.957 112.616 98.9276 117.694 88.8202 117.694L36.9647 117.694C26.8573 117.694 21.828 112.616 21.828 102.362L21.828 67.3947C23.9408 69.2087 26.911 69.8726 29.6893 69.0624L29.6893 102.264C29.6893 107.147 32.2772 109.833 37.3554 109.833L88.4784 109.833C93.5077 109.833 96.0956 107.147 96.0956 102.264L96.0956 43.2311C96.0956 38.3482 93.5077 35.6627 88.4296 35.6627L44.2584 35.6627C44.1673 33.7229 43.3799 31.757 41.7499 30.0963L39.5161 27.8014L88.8202 27.8014C98.9276 27.8014 103.957 32.8795 103.957 43.1334Z" fill="currentColor"/>
  104. <path d="M83.3026 91.9127C83.3026 93.5728 82.0331 94.8424 80.2753 94.8424L44.3378 94.8424C42.58 94.8424 41.2616 93.5728 41.2616 91.9127C41.2616 90.2037 42.58 88.8365 44.3378 88.8365L80.2753 88.8365C82.0331 88.8365 83.3026 90.2037 83.3026 91.9127Z" fill="currentColor"/>
  105. <path d="M83.3026 72.6744C83.3026 74.3834 82.0331 75.7506 80.2753 75.7506L44.3378 75.7506C42.58 75.7506 41.2616 74.3834 41.2616 72.6744C41.2616 71.0143 42.58 69.7447 44.3378 69.7447L80.2753 69.7447C82.0331 69.7447 83.3026 71.0143 83.3026 72.6744Z" fill="currentColor"/>
  106. <path d="M83.3026 53.5826C83.3026 55.2916 82.0331 56.61 80.2753 56.61L44.3378 56.61C42.58 56.61 41.2616 55.2916 41.2616 53.5826C41.2616 51.9225 42.58 50.6041 44.3378 50.6041L80.2753 50.6041C82.0331 50.6041 83.3026 51.9225 83.3026 53.5826Z" fill="currentColor"/>
  107. <path d="M6.64247 47.5768C6.59365 49.4322 8.83974 50.067 10.0604 48.8463L17.5311 41.3756L25.6854 61.5904C26.0761 62.5182 27.0526 62.9576 27.9315 62.6158L32.8143 60.6627C33.6933 60.2721 34.0351 59.2467 33.5956 58.3189L25.0018 38.4459L35.4999 38.0064C37.3554 37.9088 38.2831 36.151 36.9159 34.735L10.2558 7.29355C9.03505 6.07285 7.13075 6.75644 7.08193 8.56308Z" fill="currentColor"/>
  108. </svg>`.trim();
  109.  
  110. let isScriptActive = SCRIPT_SETTINGS.DEFAULT_ACTIVATION_STATE;
  111. let currentLocale = "en-US";
  112. let localizedStrings = UI_TEXTS["en-US"];
  113. let userScriptMenuCommandId = null;
  114. let overrideStyleSheetElement = null;
  115. let edgeIndicatorElement = null;
  116. let domMutationObserver = null;
  117. let statusNotificationTimer = null;
  118. let statusNotificationRemovalTimer = null;
  119. let indicatorCollapseTimer = null;
  120. const pageOrigin = window.location.origin;
  121.  
  122. function injectCoreUIStyles() {
  123. const appleEaseOutQuint = "cubic-bezier(0.23, 1, 0.32, 1)";
  124. const appleEaseOutStandard = "cubic-bezier(0, 0, 0.58, 1)";
  125. const animationDuration = SCRIPT_SETTINGS.ANIMATION_DURATION_MS;
  126. const indicatorTransitionDuration = "0.25s";
  127. const iconTransitionDuration = "0.2s";
  128. const iconTransitionDelay = "0.1s";
  129.  
  130. const baseCSS = `
  131. :root {
  132. --ctp-frappe-rosewater: #f2d5cf;
  133. --ctp-frappe-flamingo: #eebebe;
  134. --ctp-frappe-pink: #f4b8e4;
  135. --ctp-frappe-mauve: #ca9ee6;
  136. --ctp-frappe-red: #e78284;
  137. --ctp-frappe-maroon: #ea999c;
  138. --ctp-frappe-peach: #ef9f76;
  139. --ctp-frappe-yellow: #e5c890;
  140. --ctp-frappe-green: #a6d189;
  141. --ctp-frappe-teal: #81c8be;
  142. --ctp-frappe-sky: #99d1db;
  143. --ctp-frappe-sapphire: #85c1dc;
  144. --ctp-frappe-blue: #8caaee;
  145. --ctp-frappe-lavender: #babbf1;
  146. --ctp-frappe-text: #c6d0f5;
  147. --ctp-frappe-subtext1: #b5bfe2;
  148. --ctp-frappe-subtext0: #a5adce;
  149. --ctp-frappe-overlay2: #949cbb;
  150. --ctp-frappe-overlay1: #838ba7;
  151. --ctp-frappe-overlay0: #737994;
  152. --ctp-frappe-surface2: #626880;
  153. --ctp-frappe-surface1: #51576d;
  154. --ctp-frappe-surface0: #414559;
  155. --ctp-frappe-base: #303446;
  156. --ctp-frappe-mantle: #292c3c;
  157. --ctp-frappe-crust: #232634;
  158.  
  159. --ctp-latte-rosewater: #dc8a78;
  160. --ctp-latte-flamingo: #dd7878;
  161. --ctp-latte-pink: #ea76cb;
  162. --ctp-latte-mauve: #8839ef;
  163. --ctp-latte-red: #d20f39;
  164. --ctp-latte-maroon: #e64553;
  165. --ctp-latte-peach: #fe640b;
  166. --ctp-latte-yellow: #df8e1d;
  167. --ctp-latte-green: #40a02b;
  168. --ctp-latte-teal: #179299;
  169. --ctp-latte-sky: #04a5e5;
  170. --ctp-latte-sapphire: #209fb5;
  171. --ctp-latte-blue: #1e66f5;
  172. --ctp-latte-lavender: #7287fd;
  173. --ctp-latte-text: #4c4f69;
  174. --ctp-latte-subtext1: #5c5f77;
  175. --ctp-latte-subtext0: #6c6f85;
  176. --ctp-latte-overlay2: #7c7f93;
  177. --ctp-latte-overlay1: #8c8fa1;
  178. --ctp-latte-overlay0: #9ca0b0;
  179. --ctp-latte-surface2: #acb0be;
  180. --ctp-latte-surface1: #bcc0cc;
  181. --ctp-latte-surface0: #ccd0da;
  182. --ctp-latte-base: #eff1f5;
  183. --ctp-latte-mantle: #e6e9ef;
  184. --ctp-latte-crust: #dce0e8;
  185.  
  186. --wl-notify-bg-dark: rgba(48, 52, 70, 0.88);
  187. --wl-notify-text-dark: var(--ctp-frappe-text);
  188. --wl-notify-title-dark: var(--ctp-frappe-text);
  189. --wl-indicator-bg-dark: rgba(65, 69, 89, 0.3);
  190. --wl-indicator-hover-bg-dark: rgba(81, 87, 109, 0.5);
  191. --wl-indicator-expanded-bg-dark: rgba(48, 52, 70, 0.85);
  192. --wl-indicator-active-bg-dark: rgba(166, 209, 137, 0.88);
  193. --wl-border-dark: rgba(98, 104, 128, 0.2);
  194. --wl-icon-color-dark: var(--ctp-frappe-text);
  195. --wl-icon-active-color-dark: var(--ctp-frappe-crust);
  196.  
  197. --wl-notify-bg-light: rgba(239, 241, 245, 0.88);
  198. --wl-notify-text-light: var(--ctp-latte-text);
  199. --wl-notify-title-light: var(--ctp-latte-text);
  200. --wl-indicator-bg-light: rgba(204, 208, 218, 0.3);
  201. --wl-indicator-hover-bg-light: rgba(188, 192, 204, 0.5);
  202. --wl-indicator-expanded-bg-light: rgba(239, 241, 245, 0.85);
  203. --wl-indicator-active-bg-light: rgba(64, 160, 43, 0.88);
  204. --wl-border-light: rgba(172, 176, 190, 0.2);
  205. --wl-icon-color-light: var(--ctp-latte-text);
  206. --wl-icon-active-color-light: var(--ctp-latte-base);
  207.  
  208. --wl-shadow-dark:
  209. 0 1px 3px rgba(0, 0, 0, 0.15),
  210. 0 8px 15px rgba(0, 0, 0, 0.25);
  211. --wl-shadow-light:
  212. 0 1px 3px rgba(90, 90, 90, 0.08),
  213. 0 8px 15px rgba(90, 90, 90, 0.12);
  214. }
  215. #${ELEMENT_IDS.STATUS_NOTIFICATION} {
  216. position: fixed;
  217. top: 20px;
  218. right: -400px;
  219. z-index: 2147483646;
  220. display: flex;
  221. align-items: center;
  222. width: 310px;
  223. padding: 14px 18px;
  224. border: 1px solid var(--wl-border-dark);
  225. border-radius: 14px;
  226. background-color: var(--wl-notify-bg-dark);
  227. color: var(--wl-notify-text-dark);
  228. box-shadow: var(--wl-shadow-dark);
  229. box-sizing: border-box;
  230. opacity: 0;
  231. font-family: ${SCRIPT_SETTINGS.UI_FONT_STACK};
  232. text-align: left;
  233. backdrop-filter: blur(20px) saturate(180%);
  234. -webkit-backdrop-filter: blur(20px) saturate(180%);
  235. transition: right ${animationDuration}ms ${appleEaseOutQuint},
  236. opacity ${animationDuration * 0.8}ms ${appleEaseOutQuint};
  237. }
  238. #${ELEMENT_IDS.STATUS_NOTIFICATION}.${
  239. CSS_CLASSES.STATUS_NOTIFICATION_VISIBLE
  240. } {
  241. right: 20px;
  242. opacity: 1;
  243. }
  244. #${ELEMENT_IDS.STATUS_NOTIFICATION} .${
  245. CSS_CLASSES.STATUS_NOTIFICATION_ICON
  246. } {
  247. display: flex;
  248. align-items: center;
  249. justify-content: center;
  250. width: 30px;
  251. height: 30px;
  252. margin-right: 14px;
  253. flex-shrink: 0;
  254. color: var(--wl-icon-color-dark);
  255. }
  256. #${ELEMENT_IDS.STATUS_NOTIFICATION} .${
  257. CSS_CLASSES.STATUS_NOTIFICATION_ICON
  258. } svg {
  259. display: block;
  260. width: 100%;
  261. height: 100%;
  262. }
  263. #${ELEMENT_IDS.STATUS_NOTIFICATION} .${
  264. CSS_CLASSES.STATUS_NOTIFICATION_CONTENT
  265. } {
  266. display: flex;
  267. flex-direction: column;
  268. flex-grow: 1;
  269. min-width: 0;
  270. }
  271. #${ELEMENT_IDS.STATUS_NOTIFICATION} .${
  272. CSS_CLASSES.STATUS_NOTIFICATION_TITLE
  273. } {
  274. margin-bottom: 4px;
  275. color: var(--wl-notify-title-dark);
  276. font-size: 15px;
  277. font-weight: 600;
  278. white-space: nowrap;
  279. overflow: hidden;
  280. text-overflow: ellipsis;
  281. }
  282. #${ELEMENT_IDS.STATUS_NOTIFICATION} .${
  283. CSS_CLASSES.STATUS_NOTIFICATION_MESSAGE
  284. } {
  285. color: var(--wl-notify-text-dark);
  286. font-size: 13px;
  287. line-height: 1.45;
  288. word-wrap: break-word;
  289. overflow-wrap: break-word;
  290. }
  291. #${ELEMENT_IDS.EDGE_INDICATOR} {
  292. position: fixed;
  293. bottom: 20px;
  294. right: 20px;
  295. z-index: 2147483647;
  296. display: flex;
  297. align-items: center;
  298. justify-content: center;
  299. width: 12px;
  300. height: 12px;
  301. border-radius: 50%;
  302. background-color: var(--wl-indicator-bg-dark);
  303. color: var(--wl-icon-color-dark);
  304. opacity: 0.4;
  305. overflow: hidden;
  306. cursor: pointer;
  307. backdrop-filter: blur(3px);
  308. -webkit-backdrop-filter: blur(3px);
  309. transition: width ${indicatorTransitionDuration} ${appleEaseOutStandard},
  310. height ${indicatorTransitionDuration} ${appleEaseOutStandard},
  311. opacity ${indicatorTransitionDuration} ${appleEaseOutStandard},
  312. background-color ${indicatorTransitionDuration} ${appleEaseOutStandard},
  313. color ${indicatorTransitionDuration} ${appleEaseOutStandard},
  314. backdrop-filter ${indicatorTransitionDuration} ${appleEaseOutStandard},
  315. box-shadow ${indicatorTransitionDuration} ${appleEaseOutStandard},
  316. transform ${iconTransitionDuration} ${appleEaseOutStandard};
  317. user-select: none !important;
  318. -webkit-user-select: none !important;
  319. -moz-user-select: none !important;
  320. -ms-user-select: none !important;
  321. -webkit-user-drag: none !important;
  322. user-drag: none !important;
  323. }
  324. #${ELEMENT_IDS.EDGE_INDICATOR}:hover {
  325. background-color: var(--wl-indicator-hover-bg-dark);
  326. opacity: 0.8;
  327. transform: scale(1.1);
  328. }
  329. #${ELEMENT_IDS.EDGE_INDICATOR}.${
  330. CSS_CLASSES.EDGE_INDICATOR_ACTIVE
  331. }:not(.${CSS_CLASSES.EDGE_INDICATOR_EXPANDED}) {
  332. background-color: var(--wl-indicator-active-bg-dark);
  333. opacity: 0.6;
  334. }
  335. #${ELEMENT_IDS.EDGE_INDICATOR}.${CSS_CLASSES.EDGE_INDICATOR_EXPANDED} {
  336. width: 44px;
  337. height: 44px;
  338. border: 1px solid var(--wl-border-dark);
  339. background-color: var(--wl-indicator-expanded-bg-dark);
  340. box-shadow: var(--wl-shadow-dark);
  341. opacity: 1;
  342. backdrop-filter: blur(16px) saturate(180%);
  343. -webkit-backdrop-filter: blur(16px) saturate(180%);
  344. transform: scale(1);
  345. }
  346. #${ELEMENT_IDS.EDGE_INDICATOR}.${
  347. CSS_CLASSES.EDGE_INDICATOR_EXPANDED
  348. }:hover {
  349. transform: scale(1.08);
  350. }
  351. #${ELEMENT_IDS.EDGE_INDICATOR}.${CSS_CLASSES.EDGE_INDICATOR_EXPANDED}.${
  352. CSS_CLASSES.EDGE_INDICATOR_ACTIVE
  353. } {
  354. background-color: var(--wl-indicator-active-bg-dark);
  355. color: var(--wl-icon-active-color-dark);
  356. }
  357. #${ELEMENT_IDS.EDGE_INDICATOR} svg {
  358. display: block;
  359. width: 22px;
  360. height: 22px;
  361. opacity: 0;
  362. transform: scale(0.5);
  363. transition: opacity ${iconTransitionDuration} ${appleEaseOutStandard} ${iconTransitionDelay},
  364. transform ${iconTransitionDuration} ${appleEaseOutStandard} ${iconTransitionDelay};
  365. pointer-events: none;
  366. }
  367. #${ELEMENT_IDS.EDGE_INDICATOR}.${
  368. CSS_CLASSES.EDGE_INDICATOR_EXPANDED
  369. } svg {
  370. opacity: 1;
  371. transform: scale(1);
  372. }
  373. @media (prefers-color-scheme: light) {
  374. #${ELEMENT_IDS.STATUS_NOTIFICATION} {
  375. border: 1px solid var(--wl-border-light);
  376. background-color: var(--wl-notify-bg-light);
  377. color: var(--wl-notify-text-light);
  378. box-shadow: var(--wl-shadow-light);
  379. }
  380. #${ELEMENT_IDS.STATUS_NOTIFICATION} .${
  381. CSS_CLASSES.STATUS_NOTIFICATION_ICON
  382. } {
  383. color: var(--wl-icon-color-light);
  384. }
  385. #${ELEMENT_IDS.STATUS_NOTIFICATION} .${
  386. CSS_CLASSES.STATUS_NOTIFICATION_TITLE
  387. } {
  388. color: var(--wl-notify-title-light);
  389. }
  390. #${ELEMENT_IDS.STATUS_NOTIFICATION} .${
  391. CSS_CLASSES.STATUS_NOTIFICATION_MESSAGE
  392. } {
  393. color: var(--wl-notify-text-light);
  394. }
  395. #${ELEMENT_IDS.EDGE_INDICATOR} {
  396. background-color: var(--wl-indicator-bg-light);
  397. color: var(--wl-icon-color-light);
  398. }
  399. #${ELEMENT_IDS.EDGE_INDICATOR}:hover {
  400. background-color: var(--wl-indicator-hover-bg-light);
  401. }
  402. #${ELEMENT_IDS.EDGE_INDICATOR}.${
  403. CSS_CLASSES.EDGE_INDICATOR_ACTIVE
  404. }:not(.${CSS_CLASSES.EDGE_INDICATOR_EXPANDED}) {
  405. background-color: var(--wl-indicator-active-bg-light);
  406. opacity: 0.6;
  407. }
  408. #${ELEMENT_IDS.EDGE_INDICATOR}.${
  409. CSS_CLASSES.EDGE_INDICATOR_EXPANDED
  410. } {
  411. border: 1px solid var(--wl-border-light);
  412. background-color: var(--wl-indicator-expanded-bg-light);
  413. box-shadow: var(--wl-shadow-light);
  414. }
  415. #${ELEMENT_IDS.EDGE_INDICATOR}.${
  416. CSS_CLASSES.EDGE_INDICATOR_EXPANDED
  417. }.${CSS_CLASSES.EDGE_INDICATOR_ACTIVE} {
  418. background-color: var(--wl-indicator-active-bg-light);
  419. color: var(--wl-icon-active-color-light);
  420. }
  421. }
  422. `;
  423. try {
  424. GM_addStyle(baseCSS);
  425. } catch (e) {}
  426. }
  427.  
  428. function debounce(func, wait) {
  429. let timeout;
  430. return function executedFunction(...args) {
  431. const later = () => {
  432. clearTimeout(timeout);
  433. func.apply(this, args);
  434. };
  435. clearTimeout(timeout);
  436. timeout = setTimeout(later, wait);
  437. };
  438. }
  439.  
  440. function detectUserLanguage() {
  441. const languages = navigator.languages || [navigator.language];
  442. for (const lang of languages) {
  443. const langLower = lang.toLowerCase();
  444. if (langLower === "zh-cn") return "zh-CN";
  445. if (
  446. langLower === "zh-tw" ||
  447. langLower === "zh-hk" ||
  448. langLower === "zh-mo"
  449. )
  450. return "zh-TW";
  451. if (langLower === "en-us") return "en-US";
  452. if (langLower.startsWith("zh-")) return "zh-CN";
  453. if (langLower.startsWith("en-")) return "en-US";
  454. }
  455. for (const lang of languages) {
  456. const langLower = lang.toLowerCase();
  457. if (langLower.startsWith("zh")) return "zh-CN";
  458. if (langLower.startsWith("en")) return "en-US";
  459. }
  460. return "en-US";
  461. }
  462.  
  463. function getActivationStateStorageKey() {
  464. const origin = String(pageOrigin || "").replace(/\/$/, "");
  465. return `${STORAGE_KEYS.ACTIVATION_STATE_PREFIX}${origin}`;
  466. }
  467.  
  468. function loadActivationState() {
  469. const storageKey = getActivationStateStorageKey();
  470. const defaultStateString = SCRIPT_SETTINGS.DEFAULT_ACTIVATION_STATE
  471. ? STORAGE_KEYS.STATE_ENABLED_VALUE
  472. : STORAGE_KEYS.STATE_DISABLED_VALUE;
  473. let storedValue = defaultStateString;
  474. try {
  475. storedValue = GM_getValue(storageKey, defaultStateString);
  476. } catch (e) {}
  477. if (
  478. storedValue !== STORAGE_KEYS.STATE_ENABLED_VALUE &&
  479. storedValue !== STORAGE_KEYS.STATE_DISABLED_VALUE
  480. ) {
  481. storedValue = defaultStateString;
  482. }
  483. isScriptActive = storedValue === STORAGE_KEYS.STATE_ENABLED_VALUE;
  484. }
  485.  
  486. function saveActivationState() {
  487. const storageKey = getActivationStateStorageKey();
  488. const valueToStore = isScriptActive
  489. ? STORAGE_KEYS.STATE_ENABLED_VALUE
  490. : STORAGE_KEYS.STATE_DISABLED_VALUE;
  491. try {
  492. GM_setValue(storageKey, valueToStore);
  493. } catch (e) {}
  494. }
  495.  
  496. function stopPropagationHandler(event) {
  497. event.stopImmediatePropagation();
  498. }
  499.  
  500. function registerEventInterceptors() {
  501. RESTRICTION_OVERRIDES.EVENTS_TO_INTERCEPT.forEach((type) => {
  502. document.addEventListener(type, stopPropagationHandler, {
  503. capture: true,
  504. passive: false,
  505. });
  506. });
  507. }
  508.  
  509. function unregisterEventInterceptors() {
  510. RESTRICTION_OVERRIDES.EVENTS_TO_INTERCEPT.forEach((type) => {
  511. document.removeEventListener(type, stopPropagationHandler, {
  512. capture: true,
  513. });
  514. });
  515. }
  516.  
  517. function injectRestrictionOverrideStyles() {
  518. if (
  519. overrideStyleSheetElement ||
  520. document.getElementById(ELEMENT_IDS.OVERRIDE_STYLE_SHEET)
  521. )
  522. return;
  523. const css = `
  524. *,
  525. *::before,
  526. *::after {
  527. user-select: text !important;
  528. -webkit-user-select: text !important;
  529. -moz-user-select: text !important;
  530. -ms-user-select: text !important;
  531. cursor: auto !important;
  532. -webkit-user-drag: auto !important;
  533. user-drag: auto !important;
  534. }
  535. body {
  536. cursor: auto !important;
  537. }
  538. ::selection {
  539. background-color: highlight !important;
  540. color: highlighttext !important;
  541. }
  542. ::-moz-selection {
  543. background-color: highlight !important;
  544. color: highlighttext !important;
  545. }
  546. `;
  547. overrideStyleSheetElement = document.createElement("style");
  548. overrideStyleSheetElement.id = ELEMENT_IDS.OVERRIDE_STYLE_SHEET;
  549. overrideStyleSheetElement.textContent = css;
  550. (document.head || document.documentElement).appendChild(
  551. overrideStyleSheetElement
  552. );
  553. }
  554.  
  555. function removeRestrictionOverrideStyles() {
  556. overrideStyleSheetElement?.remove();
  557. overrideStyleSheetElement = null;
  558. document.getElementById(ELEMENT_IDS.OVERRIDE_STYLE_SHEET)?.remove();
  559. }
  560.  
  561. function clearElementInlineHandlers(element) {
  562. if (!element || element.nodeType !== Node.ELEMENT_NODE) return;
  563. for (const prop of RESTRICTION_OVERRIDES.INLINE_HANDLER_ATTRIBUTES_TO_CLEAR) {
  564. if (
  565. prop in element &&
  566. (typeof element[prop] === "function" || element[prop] !== null)
  567. ) {
  568. try {
  569. element[prop] = null;
  570. } catch (e) {}
  571. }
  572. if (element.hasAttribute(prop)) {
  573. try {
  574. element.removeAttribute(prop);
  575. } catch (e) {}
  576. }
  577. }
  578. }
  579.  
  580. function clearInlineHandlersRecursively(rootNode) {
  581. if (!isScriptActive || !rootNode) return;
  582. try {
  583. if (rootNode.nodeType === Node.ELEMENT_NODE) {
  584. if (
  585. rootNode.id !== ELEMENT_IDS.EDGE_INDICATOR &&
  586. rootNode.id !== ELEMENT_IDS.STATUS_NOTIFICATION
  587. ) {
  588. clearElementInlineHandlers(rootNode);
  589. }
  590. if (rootNode.shadowRoot?.mode === "open")
  591. clearInlineHandlersRecursively(rootNode.shadowRoot);
  592. }
  593. const elements = rootNode.querySelectorAll?.("*");
  594. if (elements) {
  595. for (const element of elements) {
  596. if (
  597. element.id !== ELEMENT_IDS.EDGE_INDICATOR &&
  598. element.id !== ELEMENT_IDS.STATUS_NOTIFICATION &&
  599. !element.closest(`#${ELEMENT_IDS.EDGE_INDICATOR}`) &&
  600. !element.closest(`#${ELEMENT_IDS.STATUS_NOTIFICATION}`)
  601. ) {
  602. clearElementInlineHandlers(element);
  603. if (element.shadowRoot?.mode === "open")
  604. clearInlineHandlersRecursively(element.shadowRoot);
  605. }
  606. }
  607. }
  608. } catch (error) {}
  609. }
  610.  
  611. function handleDOMMutation(mutations) {
  612. if (!isScriptActive) return;
  613. for (const mutation of mutations) {
  614. if (mutation.type === "childList") {
  615. for (const node of mutation.addedNodes) {
  616. if (node.nodeType === Node.ELEMENT_NODE) {
  617. if (
  618. node.id !== ELEMENT_IDS.EDGE_INDICATOR &&
  619. node.id !== ELEMENT_IDS.STATUS_NOTIFICATION &&
  620. !node.closest(`#${ELEMENT_IDS.EDGE_INDICATOR}`) &&
  621. !node.closest(`#${ELEMENT_IDS.STATUS_NOTIFICATION}`)
  622. ) {
  623. clearElementInlineHandlers(node);
  624. clearInlineHandlersRecursively(node);
  625. }
  626. }
  627. }
  628. }
  629. }
  630. }
  631.  
  632. function initializeDOMObserver() {
  633. if (domMutationObserver || !document.documentElement) return;
  634. const observerOptions = { childList: true, subtree: true };
  635. domMutationObserver = new MutationObserver(handleDOMMutation);
  636. try {
  637. domMutationObserver.observe(document.documentElement, observerOptions);
  638. } catch (error) {
  639. domMutationObserver = null;
  640. }
  641. }
  642.  
  643. function disconnectDOMObserver() {
  644. if (domMutationObserver) {
  645. domMutationObserver.disconnect();
  646. domMutationObserver = null;
  647. }
  648. }
  649.  
  650. function activateRestrictionOverrides() {
  651. if (isScriptActive) return;
  652. isScriptActive = true;
  653. injectRestrictionOverrideStyles();
  654. registerEventInterceptors();
  655. clearInlineHandlersRecursively(document.documentElement);
  656. initializeDOMObserver();
  657. }
  658.  
  659. function deactivateRestrictionOverrides() {
  660. if (!isScriptActive) return;
  661. isScriptActive = false;
  662. removeRestrictionOverrideStyles();
  663. unregisterEventInterceptors();
  664. disconnectDOMObserver();
  665. }
  666.  
  667. function displayStatusNotification(messageKey) {
  668. if (statusNotificationTimer) clearTimeout(statusNotificationTimer);
  669. if (statusNotificationRemovalTimer)
  670. clearTimeout(statusNotificationRemovalTimer);
  671. statusNotificationTimer = null;
  672. statusNotificationRemovalTimer = null;
  673.  
  674. const title = localizedStrings.SCRIPT_TITLE;
  675. const message = localizedStrings[messageKey] || messageKey;
  676.  
  677. const renderNotification = () => {
  678. let notificationElement = document.getElementById(
  679. ELEMENT_IDS.STATUS_NOTIFICATION
  680. );
  681. if (!notificationElement && document.body) {
  682. notificationElement = document.createElement("div");
  683. notificationElement.id = ELEMENT_IDS.STATUS_NOTIFICATION;
  684. notificationElement.innerHTML = `
  685. <div class="${CSS_CLASSES.STATUS_NOTIFICATION_ICON}">${SVG_ICON_MARKUP}</div>
  686. <div class="${CSS_CLASSES.STATUS_NOTIFICATION_CONTENT}">
  687. <div class="${CSS_CLASSES.STATUS_NOTIFICATION_TITLE}"></div>
  688. <div class="${CSS_CLASSES.STATUS_NOTIFICATION_MESSAGE}"></div>
  689. </div>`.trim();
  690. document.body.appendChild(notificationElement);
  691. } else if (!notificationElement) return;
  692.  
  693. const titleElement = notificationElement.querySelector(
  694. `.${CSS_CLASSES.STATUS_NOTIFICATION_TITLE}`
  695. );
  696. const messageElement = notificationElement.querySelector(
  697. `.${CSS_CLASSES.STATUS_NOTIFICATION_MESSAGE}`
  698. );
  699. if (titleElement) titleElement.textContent = title;
  700. if (messageElement) messageElement.textContent = message;
  701.  
  702. notificationElement.classList.remove(
  703. CSS_CLASSES.STATUS_NOTIFICATION_VISIBLE
  704. );
  705. void notificationElement.offsetWidth;
  706.  
  707. requestAnimationFrame(() => {
  708. const currentElement = document.getElementById(
  709. ELEMENT_IDS.STATUS_NOTIFICATION
  710. );
  711. if (currentElement) {
  712. setTimeout(() => {
  713. if (document.getElementById(ELEMENT_IDS.STATUS_NOTIFICATION)) {
  714. currentElement.classList.add(
  715. CSS_CLASSES.STATUS_NOTIFICATION_VISIBLE
  716. );
  717. }
  718. }, 20);
  719. }
  720. });
  721.  
  722. statusNotificationTimer = setTimeout(() => {
  723. const currentElement = document.getElementById(
  724. ELEMENT_IDS.STATUS_NOTIFICATION
  725. );
  726. if (currentElement) {
  727. currentElement.classList.remove(
  728. CSS_CLASSES.STATUS_NOTIFICATION_VISIBLE
  729. );
  730. statusNotificationRemovalTimer = setTimeout(() => {
  731. document.getElementById(ELEMENT_IDS.STATUS_NOTIFICATION)?.remove();
  732. statusNotificationTimer = null;
  733. statusNotificationRemovalTimer = null;
  734. }, SCRIPT_SETTINGS.ANIMATION_DURATION_MS);
  735. } else {
  736. statusNotificationTimer = null;
  737. statusNotificationRemovalTimer = null;
  738. }
  739. }, SCRIPT_SETTINGS.NOTIFICATION_VISIBILITY_DURATION_MS);
  740. };
  741.  
  742. if (document.readyState === "loading") {
  743. document.addEventListener("DOMContentLoaded", renderNotification, {
  744. once: true,
  745. });
  746. } else {
  747. renderNotification();
  748. }
  749. }
  750.  
  751. function updateUserScriptMenuCommand() {
  752. if (userScriptMenuCommandId) {
  753. try {
  754. GM_unregisterMenuCommand(userScriptMenuCommandId);
  755. } catch (e) {}
  756. userScriptMenuCommandId = null;
  757. }
  758. const label = isScriptActive
  759. ? localizedStrings.STATUS_ENABLED
  760. : localizedStrings.STATUS_DISABLED;
  761. const fallbackLabel = isScriptActive
  762. ? UI_TEXTS["en-US"].STATUS_ENABLED
  763. : UI_TEXTS["en-US"].STATUS_DISABLED;
  764. const commandLabel = label || fallbackLabel;
  765. try {
  766. userScriptMenuCommandId = GM_registerMenuCommand(
  767. commandLabel,
  768. toggleActivationState
  769. );
  770. } catch (e) {
  771. userScriptMenuCommandId = null;
  772. }
  773. }
  774.  
  775. function handleEdgeIndicatorClick(event) {
  776. event.stopPropagation();
  777. clearTimeout(indicatorCollapseTimer);
  778.  
  779. toggleActivationState();
  780.  
  781. const indicator =
  782. edgeIndicatorElement ||
  783. document.getElementById(ELEMENT_IDS.EDGE_INDICATOR);
  784. if (!indicator) return;
  785.  
  786. indicator.classList.remove(CSS_CLASSES.EDGE_INDICATOR_EXPANDED);
  787. void indicator.offsetWidth;
  788.  
  789. indicator.classList.add(CSS_CLASSES.EDGE_INDICATOR_EXPANDED);
  790.  
  791. indicatorCollapseTimer = setTimeout(() => {
  792. indicator.classList.remove(CSS_CLASSES.EDGE_INDICATOR_EXPANDED);
  793. indicatorCollapseTimer = null;
  794. }, SCRIPT_SETTINGS.INDICATOR_EXPANDED_DURATION_MS);
  795. }
  796.  
  797. function createEdgeIndicator() {
  798. if (!document.body || document.getElementById(ELEMENT_IDS.EDGE_INDICATOR))
  799. return;
  800. edgeIndicatorElement = document.createElement("div");
  801. edgeIndicatorElement.id = ELEMENT_IDS.EDGE_INDICATOR;
  802. edgeIndicatorElement.title = localizedStrings.SCRIPT_TITLE;
  803. edgeIndicatorElement.innerHTML = SVG_ICON_MARKUP;
  804. edgeIndicatorElement.addEventListener("click", handleEdgeIndicatorClick);
  805. edgeIndicatorElement.dataset.listenerAttached = "true";
  806. document.body.appendChild(edgeIndicatorElement);
  807. }
  808.  
  809. function updateEdgeIndicatorPersistentState() {
  810. const indicator =
  811. edgeIndicatorElement ||
  812. document.getElementById(ELEMENT_IDS.EDGE_INDICATOR);
  813. if (indicator) {
  814. indicator.classList.toggle(
  815. CSS_CLASSES.EDGE_INDICATOR_ACTIVE,
  816. isScriptActive
  817. );
  818. }
  819. }
  820.  
  821. function ensureEdgeIndicatorExists() {
  822. if (edgeIndicatorElement && document.body?.contains(edgeIndicatorElement)) {
  823. return;
  824. }
  825. let existingIndicator = document.getElementById(ELEMENT_IDS.EDGE_INDICATOR);
  826. if (existingIndicator) {
  827. edgeIndicatorElement = existingIndicator;
  828. if (!edgeIndicatorElement.dataset.listenerAttached) {
  829. edgeIndicatorElement.addEventListener(
  830. "click",
  831. handleEdgeIndicatorClick
  832. );
  833. edgeIndicatorElement.dataset.listenerAttached = "true";
  834. }
  835. return;
  836. }
  837. if (document.body) {
  838. createEdgeIndicator();
  839. } else {
  840. document.addEventListener("DOMContentLoaded", createEdgeIndicator, {
  841. once: true,
  842. });
  843. }
  844. }
  845.  
  846. function toggleActivationState() {
  847. const wasActive = isScriptActive;
  848. if (wasActive) {
  849. deactivateRestrictionOverrides();
  850. displayStatusNotification("STATUS_DISABLED");
  851. } else {
  852. activateRestrictionOverrides();
  853. displayStatusNotification("STATUS_ENABLED");
  854. }
  855. saveActivationState();
  856. updateUserScriptMenuCommand();
  857. updateEdgeIndicatorPersistentState();
  858. }
  859.  
  860. const debouncedToggleActivationState = debounce(
  861. toggleActivationState,
  862. SCRIPT_SETTINGS.STATE_TOGGLE_DEBOUNCE_MS
  863. );
  864.  
  865. function handleKeyboardShortcut(event) {
  866. if (
  867. (event.ctrlKey || event.metaKey) &&
  868. event.altKey &&
  869. event.code === "KeyL"
  870. ) {
  871. event.preventDefault();
  872. event.stopPropagation();
  873. debouncedToggleActivationState();
  874. }
  875. }
  876.  
  877. function initializeScript() {
  878. ensureEdgeIndicatorExists();
  879. updateEdgeIndicatorPersistentState();
  880. if (isScriptActive) {
  881. activateRestrictionOverrides();
  882. }
  883. }
  884.  
  885. if (window.self === window.top) {
  886. try {
  887. injectCoreUIStyles();
  888. currentLocale = detectUserLanguage();
  889. localizedStrings = UI_TEXTS[currentLocale] || UI_TEXTS["en-US"];
  890. loadActivationState();
  891. updateUserScriptMenuCommand();
  892. document.addEventListener("keydown", handleKeyboardShortcut, {
  893. capture: true,
  894. });
  895.  
  896. if (document.readyState === "loading") {
  897. document.addEventListener("DOMContentLoaded", initializeScript, {
  898. once: true,
  899. });
  900. } else {
  901. initializeScript();
  902. }
  903. } catch (error) {}
  904. }
  905. })();