Webpage mask

A customizable mask layer above any webpage. You can use it as a privacy mask, a screensaver, a nightmode filter... and so on.

目前為 2025-02-07 提交的版本,檢視 最新版本

  1. // ==UserScript==
  2. // @name Webpage mask
  3. // @name:zh-CN 网页遮罩层
  4. // @name:en Webpage mask
  5. // @namespace https://gf.qytechs.cn/users/667968-pyudng
  6. // @version 0.4.1
  7. // @description A customizable mask layer above any webpage. You can use it as a privacy mask, a screensaver, a nightmode filter... and so on.
  8. // @description:zh-CN 在网页上方添加一个可以自定义的遮罩层。可以用来遮挡隐私内容,或者用作屏保,又或是用来设置护眼模式... 等等等等
  9. // @description:en A customizable mask layer above any webpage. You can use it as a privacy mask, a screensaver, a nightmode filter... and so on.
  10. // @author PY-DNG
  11. // @license MIT
  12. // @match http*://*/*
  13. // @require https://update.gf.qytechs.cn/scripts/456034/1532680/Basic%20Functions%20%28For%20userscripts%29.js
  14. // @grant GM_registerMenuCommand
  15. // @grant GM_getValue
  16. // @grant GM_setValue
  17. // @grant GM_addElement
  18. // @run-at document-start
  19. // ==/UserScript==
  20.  
  21. /* eslint-disable no-multi-spaces */
  22. /* eslint-disable no-return-assign */
  23.  
  24. /* global LogLevel DoLog Err $ $All $CrE $AEL $$CrE addStyle detectDom destroyEvent copyProp copyProps parseArgs escJsStr replaceText getUrlArgv dl_browser dl_GM AsyncManager queueTask FunctionLoader loadFuncs require isLoaded */
  25.  
  26. /* Important note: this script is for convenience, but is NOT a security tool.
  27. ANYONE with basic web programming knowledge CAN EASYILY UNLOCK/UNCENSOR/REMOVE MASK
  28. without permission/password AND EVEN YOU CANNOT KNOW IT */
  29.  
  30. (function __MAIN__() {
  31. 'use strict';
  32.  
  33. const CONST = {
  34. TextAllLang: {
  35. DEFAULT: 'en',
  36. 'zh-CN': {
  37. CompatAlert: '用户脚本 [网页遮罩层] 提示:\n(本提示仅展示一次)本脚本推荐使用最新版Tampermonkey运行,如果使用旧版Tampermonkey或其他脚本管理器可能导致兼容性问题,请注意。',
  38. Mask: '开启',
  39. Unmask: '关闭',
  40. EnableAutomask: '为此网站开启自动遮罩',
  41. DisableAutomask: '关闭此网站的自动遮罩',
  42. SetIDLETime: '设置自动遮罩触发时间',
  43. PromptIDLETime: '每当 N 秒无操作后,将为网站自动开启遮罩\n您希望 N 为:',
  44. TamperorilyDisable: '暂时禁用遮罩层',
  45. TamperorilyDisabled: '已暂时禁用遮罩层:当前网页在下一次刷新前,都不会展示遮罩层',
  46. CustomUserstyle: '自定义遮罩层样式',
  47. PromptUserstyle: '您可以在此彻底地自定义遮罩层\n如果您不确定怎么写或者不小心写错了,留空并点击确定即可重置为默认值\n\n格式:\ncss:CSS值 - 设定自定义CSS样式\nimg:网址 - 在遮罩层上全屏显示网址对应的图片\njs:代码 - 执行自定义js代码,您可以使用js:debugger测试运行环境、调试您的代码',
  48. IDLETimeInvalid: '您的输入不正确:只能输入大于等于零的整数或小数'
  49. },
  50. 'en': {
  51. CompatAlert: '(This is a one-time alert)\nFrom userscript [Privacy mask]:\nThis userscript is designed for latest versions of Tampermonkey, working with old versions or other script manager may encounter bugs.',
  52. Mask: 'Show mask',
  53. Unmask: 'Hide mask',
  54. EnableAutomask: 'Enable auto-mask for this site',
  55. DisableAutomask: 'Disable auto-mask for this site',
  56. SetIDLETime: 'Configure auto-mask time',
  57. PromptIDLETime: 'Mask will be shown after the webpage has been idle for N second(s).\n You can set that N here:',
  58. TamperorilyDisable: 'Tamperorily disable mask',
  59. TamperorilyDisabled: 'Mask tamperorily disabled: mask will not be shown in current webpage before refreshing the webpage',
  60. CustomUserstyle: 'Custom auto-mask style',
  61. PromptUserstyle: 'You can custom the content and style of the mask here\nIf you\'re not sure how to compose styles, leave it blank to set back to default.\n\nStyle format:\ncss:CSS - Apply custom css stylesheet\nimg:url - Display custom image by fullscreen\njs:code - Execute custom javascript when mask created. You can use "js:debugger" to test your code an the environment.',
  62. IDLETimeInvalid: 'Invalid input: positive numbers only'
  63. }
  64. },
  65. Style: {
  66. BuiltinStyle: '#mask {position: fixed; top: 0; left: 0; right: 100vw; bottom: 100vh; width: 100vw; height: 100vh; border: 0; margin: 0; padding: 0; background: transparent; z-index: 2147483647; display: none} #mask.show {display: block;}',
  67. DefaultUserstyle: 'css:#mask {backdrop-filter: blur(30px);}'
  68. }
  69. };
  70.  
  71. // Init language
  72. const i18n = Object.keys(CONST.TextAllLang).includes(navigator.language) ? navigator.language : CONST.TextAllLang.DEFAULT;
  73. CONST.Text = CONST.TextAllLang[i18n];
  74.  
  75. loadFuncs([{
  76. id: 'mask',
  77. desc: 'Core: create mask DOM, provide basic mask api',
  78. detectDom: 'body',
  79. dependencies: 'utils',
  80. func: () => {
  81. const utils = require('utils');
  82. const return_obj = new EventTarget();
  83.  
  84. // Make mask
  85. const mask_container = $$CrE({
  86. tagName: 'div',
  87. styles: { all: 'initial' }
  88. });
  89. const mask = $$CrE({
  90. tagName: 'div',
  91. props: { id: 'mask' }
  92. });
  93. const shadow = mask_container.attachShadow({ mode: unsafeWindow.isPY_DNG ? 'open' : 'closed' });
  94. shadow.appendChild(mask);
  95. document.body.after(mask_container);
  96.  
  97. // Styles
  98. const style = addStyle(shadow, CONST.Style.BuiltinStyle, 'mask-builtin-style');
  99. applyUserstyle();
  100.  
  101. ['mouseup', 'keyup', 'dragenter'].forEach(evtname => utils.$AEL(unsafeWindow, evtname, e => hide()));
  102.  
  103. const return_props = {
  104. mask_container, element: mask, shadow, style, show, hide,
  105. get showing() { return showing(); },
  106. set showing(v) { v ? show() : hide() },
  107. get userstyle() { return getUserstyle() },
  108. set userstyle(v) { return setUserstyle(v) }
  109. };
  110. utils.copyPropDescs(return_props, return_obj);
  111. return return_obj;
  112.  
  113. function show() {
  114. const defaultNotPrevented = return_obj.dispatchEvent(new Event('show', { cancelable: true }));
  115. defaultNotPrevented && mask.classList.add('show');
  116. }
  117.  
  118. function hide() {
  119. const defaultNotPrevented = return_obj.dispatchEvent(new Event('hide', { cancelable: true }));
  120. defaultNotPrevented && mask.classList.remove('show');
  121. }
  122.  
  123. function showing() {
  124. return mask.classList.contains('show');
  125. }
  126.  
  127. function getUserstyle() {
  128. return GM_getValue('userstyle', CONST.Style.DefaultUserstyle);
  129. }
  130.  
  131. function setUserstyle(val) {
  132. const defaultNotPrevented = return_obj.dispatchEvent(new Event('restyle', { cancelable: true }));
  133. if (defaultNotPrevented) {
  134. applyUserstyle(val);
  135. GM_setValue('userstyle', val);
  136. }
  137. }
  138.  
  139. function applyUserstyle(val) {
  140. if (!val) { val = getUserstyle() }
  141. if (!val.includes(':')) { Err('mask.applyUserStyle: type not found') }
  142. const type = val.substring(0, val.indexOf(':')).toLowerCase();
  143. const value = val.substring(val.indexOf(':') + 1).trim();
  144. switch (type) {
  145. case 'css':
  146. GM_addElement(shadow, 'style', { textContent: value, style: 'user' });
  147. utils.$AEL(return_obj, 'restyle', e => $(shadow, 'style[style="user"]').remove(), { once: true });
  148. break;
  149. case 'js':
  150. case 'javascript':
  151. utils.exec(value, { require, CONST });
  152. break;
  153. case 'img':
  154. case 'image': {
  155. addImage(value);
  156. break;
  157.  
  158. function addImage(src, remaining_retry=3) {
  159. const img = GM_addElement(mask, 'img', { src: value, style: 'width: 100vw; height: 100vh; border: 0; padding: 0; margin: 0;' }) ?? $(mask, 'img');
  160. const controller = new AbortController();
  161. utils.$AEL(img, 'error', e => {
  162. if (remaining_retry-- > 0) {
  163. DoLog(LogLevel.Warning, `Mask image load error, retrying...\n(remaining ${remaining_retry} retries)`);
  164. controller.abort();
  165. img.remove();
  166. addImage(src, remaining_retry);
  167. } else {
  168. DoLog(LogLevel.Error, `Mask image load error (after maximum retries)\nTry reloading the page or changing an image\nImage url: ${src}`);
  169. }
  170. }, { once: true });
  171. utils.$AEL(return_obj, 'restyle', e => img.remove(), {
  172. once: true,
  173. signal: controller.signal
  174. });
  175. }
  176. }
  177. default:
  178. Error(`mask.applyUserStyle: Unknown type: ${type}`);
  179. }
  180. }
  181. }
  182. }, {
  183. id: 'control',
  184. desc: 'Provide mask control ui to user',
  185. dependencies: ['utils', 'mask'],
  186. func: () => {
  187. const utils = require('utils');
  188. const mask = require('mask');
  189.  
  190. // Switch menu builder
  191. const buildMenu = (text_getter, callback, id=null) => GM_registerMenuCommand(text_getter(), callback, id !== null ? { id } : {});
  192.  
  193. // Enable/Disable switch
  194. const show_text_getter = () => CONST.Text[mask.showing ? 'Unmask' : 'Mask'];
  195. const show_menu_onclick = e => mask.showing = !mask.showing;
  196. const buildShowMenu = (id = null) => buildMenu(show_text_getter, show_menu_onclick, id);
  197. const id = buildShowMenu();
  198. utils.$AEL(mask, 'show', e => setTimeout(() => buildShowMenu(id)));
  199. utils.$AEL(mask, 'hide', e => setTimeout(() => buildShowMenu(id)));
  200.  
  201. // Tamperorily disable
  202. GM_registerMenuCommand(CONST.Text.TamperorilyDisable, e => {
  203. mask.hide();
  204. utils.$AEL(mask, 'show', e => e.preventDefault());
  205. DoLog(LogLevel.Success, CONST.Text.TamperorilyDisabled);
  206. setTimeout(() => alert(CONST.Text.TamperorilyDisabled));
  207. });
  208.  
  209. // Custom user style
  210. GM_registerMenuCommand(CONST.Text.CustomUserstyle, e => {
  211. let style = prompt(CONST.Text.PromptUserstyle, mask.userstyle);
  212. if (style === null) { return; }
  213. if (style === '') { style = CONST.Style.DefaultUserstyle }
  214. // Here should add an style valid check
  215. mask.userstyle = style;
  216. });
  217.  
  218. return { id };
  219. }
  220. }, {
  221. id: 'automask',
  222. desc: 'extension: auto-mask after certain idle time',
  223. detectDom: 'body',
  224. dependencies: ['mask', 'utils'],
  225. func: () => {
  226. const utils = require('utils');
  227. const mask = require('mask');
  228. const id = GM_registerMenuCommand(
  229. isAutomaskEnabled() ? CONST.Text.DisableAutomask : CONST.Text.EnableAutomask,
  230. function onClick(e) {
  231. isAutomaskEnabled() ? disable() : enable();
  232. GM_registerMenuCommand(
  233. isAutomaskEnabled() ? CONST.Text.DisableAutomask : CONST.Text.EnableAutomask,
  234. onClick, { id }
  235. );
  236. isAutomaskEnabled() && check_idle();
  237. }
  238. );
  239. GM_registerMenuCommand(CONST.Text.SetIDLETime, e => {
  240. const config = getConfig();
  241. const time = prompt(CONST.Text.PromptIDLETime, config.idle_time);
  242. if (time === null) { return; }
  243. if (!/^(\d+\.)?\d+$/.test(time)) { alert(CONST.Text.IDLETimeInvalid); return; }
  244. config.idle_time = +time;
  245. setConfig(config);
  246. });
  247.  
  248. // Auto-mask when idle
  249. let last_refreshed = Date.now();
  250. const cancel_idle = () => {
  251. // Iframe events won't bubble into parent window, so manually tell top window to also cancel_idle
  252. const in_iframe = unsafeWindow !== unsafeWindow.top;
  253. in_iframe && utils.postMessage(unsafeWindow.top, 'iframe_cancel_idle');
  254. // Refresh time record
  255. last_refreshed = Date.now();
  256. };
  257. ['mousemove', 'mousedown', 'mouseup', 'wheel', 'keydown', 'keyup'].forEach(evt_name =>
  258. utils.$AEL(unsafeWindow, evt_name, e => cancel_idle(), { capture: true }));
  259. utils.recieveMessage('iframe_cancel_idle', e => cancel_idle());
  260. const check_idle = () => {
  261. const config = getConfig();
  262. const time_left = config.idle_time * 1000 - (Date.now() - last_refreshed);
  263. if (time_left <= 0) {
  264. isAutomaskEnabled() && !mask.showing && mask.show();
  265. utils.$AEL(mask, 'hide', e => {
  266. cancel_idle();
  267. check_idle();
  268. }, { once: true });
  269. } else {
  270. setTimeout(check_idle, time_left);
  271. }
  272. }
  273. check_idle();
  274.  
  275. return {
  276. id, enable, disable,
  277. get enabled() { return isAutomaskEnabled(); }
  278. };
  279.  
  280. function getConfig() {
  281. return GM_getValue('automask', {
  282. sites: [],
  283. idle_time: 30
  284. });
  285. }
  286.  
  287. function setConfig(val) {
  288. return GM_setValue('automask', val);
  289. }
  290.  
  291. function isAutomaskEnabled() {
  292. return getConfig().sites.includes(location.host);
  293. }
  294.  
  295. function enable() {
  296. if (isAutomaskEnabled()) { return; }
  297. const config = getConfig();
  298. config.sites.push(location.host);
  299. setConfig(config);
  300. }
  301.  
  302. function disable() {
  303. if (!isAutomaskEnabled()) { return; }
  304. const config = getConfig();
  305. config.sites.splice(config.sites.indexOf(location.host), 1);
  306. setConfig(config);
  307. }
  308. }
  309. }, {
  310. id: 'utils',
  311. desc: 'helper functions',
  312. func: () => {
  313. function GM_hasVersion(version) {
  314. return hasVersion(GM_info?.version || '0', version);
  315.  
  316. function hasVersion(ver1, ver2) {
  317. return compareVersions(ver1.toString(), ver2.toString()) >= 0;
  318.  
  319. // https://gf.qytechs.cn/app/javascript/versioncheck.js
  320. // https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/version/format
  321. function compareVersions(a, b) {
  322. if (a == b) {
  323. return 0;
  324. }
  325. let aParts = a.split('.');
  326. let bParts = b.split('.');
  327. for (let i = 0; i < aParts.length; i++) {
  328. let result = compareVersionPart(aParts[i], bParts[i]);
  329. if (result != 0) {
  330. return result;
  331. }
  332. }
  333. // If all of a's parts are the same as b's parts, but b has additional parts, b is greater.
  334. if (bParts.length > aParts.length) {
  335. return -1;
  336. }
  337. return 0;
  338. }
  339.  
  340. function compareVersionPart(partA, partB) {
  341. let partAParts = parseVersionPart(partA);
  342. let partBParts = parseVersionPart(partB);
  343. for (let i = 0; i < partAParts.length; i++) {
  344. // "A string-part that exists is always less than a string-part that doesn't exist"
  345. if (partAParts[i].length > 0 && partBParts[i].length == 0) {
  346. return -1;
  347. }
  348. if (partAParts[i].length == 0 && partBParts[i].length > 0) {
  349. return 1;
  350. }
  351. if (partAParts[i] > partBParts[i]) {
  352. return 1;
  353. }
  354. if (partAParts[i] < partBParts[i]) {
  355. return -1;
  356. }
  357. }
  358. return 0;
  359. }
  360.  
  361. // It goes number, string, number, string. If it doesn't exist, then
  362. // 0 for numbers, empty string for strings.
  363. function parseVersionPart(part) {
  364. if (!part) {
  365. return [0, "", 0, ""];
  366. }
  367. let partParts = /([0-9]*)([^0-9]*)([0-9]*)([^0-9]*)/.exec(part)
  368. return [
  369. partParts[1] ? parseInt(partParts[1]) : 0,
  370. partParts[2],
  371. partParts[3] ? parseInt(partParts[3]) : 0,
  372. partParts[4]
  373. ];
  374. }
  375. }
  376. }
  377.  
  378. function copyPropDescs(from, to) {
  379. Object.defineProperties(to, Object.getOwnPropertyDescriptors(from));
  380. }
  381.  
  382. function randint(min, max) {
  383. return Math.random() * (max - min) + min;
  384. }
  385.  
  386. function randstr(len) {
  387. const letters = [...Array(26).keys()].map( i => String.fromCharCode('a'.charCodeAt(0) + i) );
  388. let str = '';
  389. for (let i = 0; i < len; i++) {
  390. str += letters.at(randint(0, 25));
  391. }
  392. return str;
  393. }
  394.  
  395. /**
  396. * execute js code in a global function closure and try to bypass CSP rules
  397. * { name: 'John', number: 123456 } will be executed by (function(name, number) { code }) ('John', 123456);
  398. *
  399. * @param {string} code
  400. * @param {Object} args
  401. */
  402. function exec(code, args) {
  403. // Parse args
  404. const arg_names = Object.keys(args);
  405. const arg_vals = arg_names.map(name => args[name]);
  406. // Construct middle code and middle obj
  407. const id = randstr(16);
  408. const middle_obj = unsafeWindow[id] = { id, arg_vals, url: null };
  409. const middle_code_parts = {
  410. single_instance: [
  411. '// Run code only once',
  412. `if (!window.hasOwnProperty('${id}')) { return; }`
  413. ].join('\n'),
  414. cleaner: [
  415. '// Do some cleaning first',
  416. `const middle_obj = window.${id};`,
  417. `delete window.${id};`,
  418. `URL.revokeObjectURL(middle_obj.url);`,
  419. `document.querySelector('#${id}')?.remove();`
  420. ].join('\n'),
  421. executer: [
  422. '// Execute user code',
  423. `(function(${arg_names.join(', ')}, middle_obj) {`,
  424. code,
  425. `}).call(null, ...middle_obj.arg_vals, undefined);`
  426. ].join('\n'),
  427. }
  428. const middle_code = `(function() {\n${Object.values(middle_code_parts).join('\n\n')}\n}) ();`;
  429. const blob = new Blob([middle_code], { type: 'application/javascript' });
  430. const url = middle_obj.url = URL.createObjectURL(blob);
  431. // Create and execute <script>
  432. GM_addElement(document.head, 'script', { src: url, id });
  433. GM_addElement(document.head, 'script', { textContent: middle_code, id });
  434. }
  435.  
  436. /**
  437. * Some website (like icourse163.com, dolmods.com, etc.) hooked addEventListener,\
  438. * so calling target.addEventListener has no effect.
  439. * this function get a "pure" addEventListener from new iframe, and make use of it
  440. */
  441. function $AEL(target, ...args) {
  442. if (!$AEL.id_prop) {
  443. $AEL.id_prop = randstr(16);
  444. $AEL.id_val = randstr(16);
  445. GM_addElement(document.body, 'iframe', {
  446. srcdoc: '<html></html>',
  447. style: [
  448. 'border: 0',
  449. 'padding: 0',
  450. 'margin: 0',
  451. 'width: 0',
  452. 'height: 0',
  453. 'display: block',
  454. 'visibility: visible'
  455. ].concat('').join(' !important; '),
  456. [$AEL.id_prop]: $AEL.id_val,
  457. });
  458. }
  459. try {
  460. const ifr = $(`[${$AEL.id_prop}=${$AEL.id_val}]`);
  461. const AEL = ifr.contentWindow.EventTarget.prototype.addEventListener;
  462. return AEL.call(target, ...args);
  463. } catch (e) {
  464. if (!$(`[${$AEL.id_prop}=${$AEL.id_val}]`)) {
  465. DoLog(LogLevel.Warning, 'GM_addElement is not working properly: added iframe not found\nUsing normal addEventListener instead');
  466. } else {
  467. DoLog(LogLevel.Warning, 'Unknown error occured\nUsing normal addEventListener instead')
  468. }
  469. return window.EventTarget.prototype.addEventListener.call(target, ...args);
  470. }
  471. }
  472.  
  473. const [postMessage, recieveMessage] = (function() {
  474. // Check and init security key
  475. let securityKey = GM_getValue('Message-Security-Key');
  476. if (!securityKey) {
  477. securityKey = { prop: randstr(8), value: randstr(16) };
  478. GM_setValue('Message-Security-Key', securityKey);
  479. }
  480.  
  481. /**
  482. * post a message to target window using window.postMessage
  483. * name, data will be formed as an object in format of { name, data, securityKeyProp: securityKeyVal }
  484. * securityKey will be randomly generated with first initialization
  485. * and saved with GM_setValue('Message-Security-Key', { prop, value })
  486. *
  487. * @param {string} name - type of this message
  488. * @param {*} [data] - data of this message
  489. */
  490. function postMessage(targetWindow, name, data=null) {
  491. const securityKeyProp = securityKey.prop;
  492. const securityKeyVal = securityKey.value;
  493. targetWindow.postMessage({ name, data, [securityKey.prop]: securityKey.value }, '*');
  494. }
  495.  
  496. /**
  497. * recieve message posted by postMessage
  498. * @param {string} name - which kind of message you want to recieve
  499. * @param {function} [callback] - if provided, return undefined and call this callback function each time a message
  500. recieved; if not, returns a Promise that will be fulfilled with next message for once
  501. * @returns {(Promise<number>|undefined)}
  502. */
  503. function recieveMessage(name, callback=null) {
  504. const win = typeof unsafeWindow === 'object' ? unsafeWindow : window;
  505. let resolve;
  506. $AEL(win, 'message', function listener(e) {
  507. // Check security key first
  508. if (!(e?.data?.[securityKey.prop] === securityKey.value)) { return; }
  509. if (e?.data?.name === name) {
  510. if (callback) {
  511. callback(e);
  512. } else {
  513. resolve(e);
  514. }
  515. }
  516. });
  517. if (!callback) {
  518. return new Promise((res, rej) => { resolve = res; });
  519. }
  520. }
  521.  
  522. return [postMessage, recieveMessage];
  523. }) ();
  524.  
  525. return { GM_hasVersion, copyPropDescs, randint, randstr, exec, $AEL, postMessage, recieveMessage };
  526. }
  527. }, {
  528. desc: 'compatibility alert',
  529. dependencies: 'utils',
  530. func: () => {
  531. const utils = require('utils');
  532. if (!GM_getValue('compat-alert') && (GM_info.scriptHandler !== 'Tampermonkey' || !utils.GM_hasVersion('5.0'))) {
  533. alert(CONST.Text.CompatAlert);
  534. GM_setValue('compat-alert', true);
  535. }
  536. }
  537. }]);
  538. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址