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.

目前為 2024-09-08 提交的版本,檢視 最新版本

/* eslint-disable no-multi-spaces */
/* eslint-disable no-return-assign */

// ==UserScript==
// @name               Webpage mask
// @name:zh-CN         网页遮罩层
// @name:en            Webpage mask
// @namespace          https://gf.qytechs.cn/users/667968-pyudng
// @version            0.1
// @description        A customizable mask layer above any webpage. You can use it as a privacy mask, a screensaver, a nightmode filter... and so on.
// @description:zh-CN  在网页上方添加一个可以自定义的遮罩层。可以用来遮挡隐私内容,或者用作屏保,又或是用来设置护眼模式... 等等等等
// @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.
// @author             PY-DNG
// @license            MIT
// @match              http*://*/*
// @require            https://update.gf.qytechs.cn/scripts/456034/1443751/Basic%20Functions%20%28For%20userscripts%29.js
// @grant              GM_registerMenuCommand
// @grant              GM_getValue
// @grant              GM_setValue
// @grant              GM_addElement
// @run-at             document-start
// ==/UserScript==

/* global LogLevel DoLog Err $ $All $CrE $AEL $$CrE addStyle detectDom destroyEvent copyProp copyProps parseArgs escJsStr replaceText getUrlArgv dl_browser dl_GM AsyncManager queueTask testChecker registerChecker loadFuncs */

/* Important note: this script is for convenience, but is NOT a security tool.
   ANYONE with basic web programming knowledge CAN EASYILY UNLOCK/UNCENSOR/REMOVE MASK
   without permission/password AND EVEN YOU CANNOT KNOW IT */

(function __MAIN__() {
    'use strict';

	const CONST = {
		TextAllLang: {
			DEFAULT: 'en',
			'zh-CN': {
                CompatAlert: '用户脚本 [网页遮罩层] 提示:\n(本提示仅展示一次)本脚本推荐使用最新版Tampermonkey运行,如果使用旧版Tampermonkey或其他脚本管理器可能导致兼容性问题,请注意。',
                Mask: '开启',
                Unmask: '关闭',
                EnableAutomask: '为此网站开启自动遮罩',
                DisableAutomask: '关闭此网站的自动遮罩',
                SetIDLETime: '设置自动遮罩触发时间',
                PromptIDLETime: '每当 N 秒无操作后,将为网站自动开启遮罩\n您希望 N 为:',
                CustomUserstyle: '自定义遮罩层样式',
                PromptUserstyle: '您可以在此彻底地自定义遮罩层\n如果您不确定怎么写或者不小心写错了,留空并点击确定即可重置为默认值\n\n格式:\ncss:CSS值 - 设定自定义CSS样式\nimg:网址 - 在遮罩层上全屏显示网址对应的图片\njs:代码 - 执行自定义js代码,您可以使用js:debugger测试运行环境、调试您的代码',
                IDLETimeInvalid: '您的输入不正确:只能输入大于等于零的整数或小数'
            },
            'en': {
                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.',
                Mask: 'Show mask',
                Unmask: 'Hide mask',
                EnableAutomask: 'Enable auto-mask for this site',
                DisableAutomask: 'Disable auto-mask for this site',
                SetIDLETime: 'Configure auto-mask time',
                PromptIDLETime: 'Mask will be shown after the webpage has been idle for N second(s).\n You can set that N here:',
                CustomUserstyle: 'Custom auto-mask style',
                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.',
                IDLETimeInvalid: 'Invalid input: positive numbers only'
            }
		},
        Style: {
            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;}',
            DefaultUserstyle: 'css:#mask {backdrop-filter: blur(30px);}'
        }
	};

	// Init language
	const i18n = Object.keys(CONST.TextAllLang).includes(navigator.language) ? navigator.language : CONST.TextAllLang.DEFAULT;
	CONST.Text = CONST.TextAllLang[i18n];

    const MODULE_NOT_LOADED = Symbol('MODULE_NOT_LOADED');
    const require = id => func_return_values.hasOwnProperty(id) ? func_return_values[id] : MODULE_NOT_LOADED;
    const func_return_values = loadFuncs([{
        id: 'mask',
        desc: 'Core: create mask DOM, provide basic mask api',
        detectDom: 'body',
        dependencies: 'utils',
        func: () => {
            const utils = require('utils');
            const return_obj = new EventTarget();

            // Make mask
            const mask_container = $$CrE({
                tagName: 'div',
                styles: { all: 'initial' }
            });
            const mask = $$CrE({
                tagName: 'div',
                props: { id: 'mask' }
            });
            const shadow = mask_container.attachShadow({ mode: unsafeWindow.isPY_DNG ? 'open' : 'closed' });
            shadow.appendChild(mask);
            document.body.after(mask_container);

            // Styles
            const style = addStyle(shadow, CONST.Style.BuiltinStyle, 'mask-builtin-style');
            applyUserstyle();

            ['mouseup', 'keyup'].forEach(evtname => $AEL(unsafeWindow, evtname, e => hide()));

            const return_props = {
                mask_container, element: mask, shadow, style, show, hide,
                get showing() { return showing(); },
                set showing(v) { v ? show() : hide() },
                get userstyle() { return getUserstyle() },
                set userstyle(v) { return setUserstyle(v) }
            };
            utils.copyPropDescs(return_props, return_obj);
            return return_obj;

            function show() {
                const defaultNotPrevented = return_obj.dispatchEvent(new Event('show', { cancelable: true }));
                defaultNotPrevented && mask.classList.add('show');
            }

            function hide() {
                const defaultNotPrevented = return_obj.dispatchEvent(new Event('hide', { cancelable: true }));
                defaultNotPrevented && mask.classList.remove('show');
            }

            function showing() {
                return mask.classList.contains('show');
            }

            function getUserstyle() {
                return GM_getValue('userstyle', CONST.Style.DefaultUserstyle);
            }

            function setUserstyle(val) {
                const defaultNotPrevented = return_obj.dispatchEvent(new Event('restyle', { cancelable: true }));
                if (defaultNotPrevented) {
                    applyUserstyle(val);
                    GM_setValue('userstyle', val);
                }
            }

            function applyUserstyle(val) {
                if (!val) { val = getUserstyle() }
                if (!val.includes(':')) { Err('mask.applyUserStyle: type not found') }
                const type = val.substring(0, val.indexOf(':')).toLowerCase();
                const value = val.substring(val.indexOf(':') + 1).trim();
                switch (type) {
                    case 'css':
                        GM_addElement(shadow, 'style', { textContent: value, style: 'user' });
                        $AEL(return_obj, 'restyle', e => $(shadow, 'style[style="user"]').remove(), { once: true });
                        break;
                    case 'js':
                    case 'javascript':
                        utils.exec(value, { require });
                        break;
                    case 'img':
                    case 'image':
                        GM_addElement(mask, 'img', { src: value, style: 'width: 100vw; height: 100vh; border: 0; padding: 0; margin: 0;' });
                        $AEL(return_obj, 'restyle', e => $(mask, 'img').remove(), { once: true });
                        break;
                    default:
                        Error(`mask.applyUserStyle: Unknown type: ${type}`);
                }
            }
        }
    }, {
        id: 'control',
        desc: 'Provide mask control ui to user',
        dependencies: 'mask',
        func: () => {
            const mask = require('mask');

            // Enable/Disable switch
            const buildMenu = (callback, id=null) => GM_registerMenuCommand(CONST.Text[mask.showing ? 'Unmask' : 'Mask'], callback, id !== null ? { id } : {});
            const menu_onclick = e => {
                mask.showing = !mask.showing;
                buildMenu(menu_onclick, id);
            }
            const id = buildMenu(menu_onclick);
            $AEL(mask, 'show', e => setTimeout(() => buildMenu(menu_onclick, id)));
            $AEL(mask, 'hide', e => setTimeout(() => buildMenu(menu_onclick, id)));

            // Custom user style
            GM_registerMenuCommand(CONST.Text.CustomUserstyle, e => {
                let style = prompt(CONST.Text.PromptUserstyle, mask.userstyle);
                if (style === null) { return; }
                if (style === '') { style = CONST.Style.DefaultUserstyle }
                // Here should add an style valid check
                mask.userstyle = style;
            });

            return { id };
        }
    }, {
        id: 'automask',
        desc: 'extension: auto-mask after certain idle time',
        detectDom: 'body',
        dependencies: 'mask',
        func: () => {
            const mask = require('mask');
            const id = GM_registerMenuCommand(
                isAutomaskEnabled() ? CONST.Text.DisableAutomask : CONST.Text.EnableAutomask,
                function onClick(e) {
                    isAutomaskEnabled() ? disable() : enable();
                    GM_registerMenuCommand(
                        isAutomaskEnabled() ? CONST.Text.DisableAutomask : CONST.Text.EnableAutomask,
                        onClick, { id }
                    );
                    isAutomaskEnabled() && check_idle();
                }
            );
            GM_registerMenuCommand(CONST.Text.SetIDLETime, e => {
                const config = getConfig();
                const time = prompt(CONST.Text.PromptIDLETime, config.idle_time);
                if (time === null) { return; }
                if (!/^(\d+\.)?\d+$/.test(time)) { alert(CONST.Text.IDLETimeInvalid); return; }
                config.idle_time = +time;
                setConfig(config);
            });

            // Auto-mask when idle
            let last_refreshed = Date.now();
            const cancel_idle = () => { last_refreshed = Date.now() };
            ['mousemove', 'mousedown', 'mouseup', 'wheel', 'keydown', 'keyup'].forEach(evt_name =>
                $AEL(unsafeWindow, evt_name, e => cancel_idle()), { capture: true });
            const check_idle = () => {
                const config = getConfig();
                const time_left = config.idle_time * 1000 - (Date.now() - last_refreshed);
                if (time_left <= 0) {
                    isAutomaskEnabled() && !mask.showing && mask.show();
                    $AEL(mask, 'hide', e => {
                        cancel_idle();
                        check_idle();
                    }, { once: true });
                } else {
                    setTimeout(check_idle, time_left);
                }
            }
            check_idle();

            return {
                id, enable, disable,
                get enabled() { return isAutomaskEnabled(); }
            };

            function getConfig() {
                return GM_getValue('automask', {
                    sites: [],
                    idle_time: 30
                });
            }

            function setConfig(val) {
                return GM_setValue('automask', val);
            }

            function isAutomaskEnabled() {
                return getConfig().sites.includes(location.host);
            }

            function enable() {
                if (isAutomaskEnabled()) { return; }
                const config = getConfig();
                config.sites.push(location.host);
                setConfig(config);
            }

            function disable() {
                if (!isAutomaskEnabled()) { return; }
                const config = getConfig();
                config.sites.splice(config.sites.indexOf(location.host), 1);
                setConfig(config);
            }
        }
    }, {
        id: 'utils',
        desc: 'helper functions',
        func: () => {
            function GM_hasVersion(version) {
                return hasVersion(GM_info?.version || '0', version);

                function hasVersion(ver1, ver2) {
                    return compareVersions(ver1.toString(), ver2.toString()) >= 0;

                    // https://gf.qytechs.cn/app/javascript/versioncheck.js
                    // https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/version/format
                    function compareVersions(a, b) {
                      if (a == b) {
                        return 0;
                      }
                      let aParts = a.split('.');
                      let bParts = b.split('.');
                      for (let i = 0; i < aParts.length; i++) {
                        let result = compareVersionPart(aParts[i], bParts[i]);
                        if (result != 0) {
                          return result;
                        }
                      }
                      // If all of a's parts are the same as b's parts, but b has additional parts, b is greater.
                      if (bParts.length > aParts.length) {
                        return -1;
                      }
                      return 0;
                    }

                    function compareVersionPart(partA, partB) {
                      let partAParts = parseVersionPart(partA);
                      let partBParts = parseVersionPart(partB);
                      for (let i = 0; i < partAParts.length; i++) {
                        // "A string-part that exists is always less than a string-part that doesn't exist"
                        if (partAParts[i].length > 0 && partBParts[i].length == 0) {
                          return -1;
                        }
                        if (partAParts[i].length == 0 && partBParts[i].length > 0) {
                          return 1;
                        }
                        if (partAParts[i] > partBParts[i]) {
                          return 1;
                        }
                        if (partAParts[i] < partBParts[i]) {
                          return -1;
                        }
                      }
                      return 0;
                    }

                    // It goes number, string, number, string. If it doesn't exist, then
                    // 0 for numbers, empty string for strings.
                    function parseVersionPart(part) {
                      if (!part) {
                        return [0, "", 0, ""];
                      }
                      let partParts = /([0-9]*)([^0-9]*)([0-9]*)([^0-9]*)/.exec(part)
                      return [
                        partParts[1] ? parseInt(partParts[1]) : 0,
                        partParts[2],
                        partParts[3] ? parseInt(partParts[3]) : 0,
                        partParts[4]
                      ];
                    }
                }
            }

            function copyPropDescs(from, to) {
                Object.defineProperties(to, Object.getOwnPropertyDescriptors(from));
            }

            function randint(min, max) {
                return Math.random() * (max - min) + min;
            }

            function randstr(len) {
                const letters = [...Array(26).keys()].map( i => String.fromCharCode('a'.charCodeAt(0) + i) );
                let str = '';
                for (let i = 0; i < len; i++) {
                    str += letters.at(randint(0, 25));
                }
                return str;
            }

            /**
             * execute js code in a global function closure and try to bypass CSP rules
             * { name: 'John', number: 123456 } will be executed by (function(name, number) { code }) ('John', 123456);
             *
             * @param {string} code
             * @param {Object} args
             */
            function exec(code, args) {
                // Parse args
                const arg_names = Object.keys(args);
                const arg_vals = arg_names.map(name => args[name]);
                // Construct middle code and middle obj
                const id = randstr(16);
                const middle_obj = unsafeWindow[id] = { id, arg_vals, url: null };
                const middle_code_parts = {
                    cleaner: [
                        '// Do some cleaning first',
                        `const middle_obj = window.${id};`,
                        `delete window.${id};`,
                        `URL.revokeObjectURL(middle_obj.url);`,
                        `document.querySelector('#${id}').remove();`
                    ].join('\n'),
                    executer: [
                        '// Execute user code',
                        `(function(${arg_names.join(', ')}, middle_obj) {`,
                        code,
                        `}).call(null, ...middle_obj.arg_vals, undefined);`
                    ].join('\n'),
                }
                const middle_code = `(function() {\n${middle_code_parts.cleaner}\n${middle_code_parts.executer}\n}) ();`;
                const blob = new Blob([middle_code], { type: 'application/javascript' });
                const url = middle_obj.url = URL.createObjectURL(blob);
                // Create and execute <script>
                GM_addElement(document.head, 'script', { src: url, id });
            }

            return { GM_hasVersion, copyPropDescs, exec };
        }
    }, {
        desc: 'compatibility alert',
        dependencies: 'utils',
        func: () => {
            const utils = require('utils');
            if (!GM_getValue('compat-alert') && (GM_info.scriptHandler !== 'Tampermonkey' || !utils.GM_hasVersion('5.0'))) {
                alert(CONST.Text.CompatAlert);
                GM_setValue('compat-alert', true);
            }
        }
    }]);
})();

QingJ © 2025

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