论坛列表显示图片

论坛列表显示图片,同时支持discuz搭建的论坛(如吾爱破解)以及phpwind搭建的论坛(如south plus)灯

// ==UserScript==
// @name         论坛列表显示图片
// @namespace    form_show_images_in_list
// @version      1.5
// @description  论坛列表显示图片,同时支持discuz搭建的论坛(如吾爱破解)以及phpwind搭建的论坛(如south plus)灯
// @license MIT
// @author       Gloduck
// @note         discuz路径匹配
// @match        *://*/forum-*.html
// @match        *://*/forum-*.html?*
// @match        *://*/forum.php
// @match        *://*/forum.php?*
// @match        *://*/*/forum-*.html
// @match        *://*/*/forum-*.html?*
// @match        *://*/*/forum.php
// @match        *://*/*/forum.php?*
// @note         phpwind路径匹配
// @match        *://*/*/thread.php
// @match        *://*/*/thread.php?*
// @match        *://*/thread.php
// @match        *://*/thread.php?*
// @note         1024路径匹配
// @match        *://*/*/thread0806.php*
// @match        *://*/thread0806.php*
// @grant        GM_xmlhttpRequest
// @grant        GM_addStyle
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_deleteValue
// @grant        GM_registerMenuCommand
// ==/UserScript==

(function () {
    'use strict';
    GM_addStyle(`
        .zoomable-image {
            cursor: pointer;
        }

        .zoomable-image.zoomed {
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            object-fit: contain;
            background: rgba(0, 0, 0, 0.9);
            z-index: 9999;
        }
    `);


    // 默认设置
    const defaultSettings = {
        enabled: false,
        lazyLoad: true,
        maxImageDisplayCount: 3,
        requestMaxDelay: 3000,
        ignoredImagePattern: []
    };

    // 当前设置变量
    let currentSettings = {};

    const settingsItems = [
        {
            label: '启用脚本',
            name: 'enabled',
            type: 'checkbox'
        },
        {
            label: '懒加载',
            name: 'lazyLoad',
            type: 'checkbox'
        },
        {
            label: '最大图片显示数量',
            name: 'maxImageDisplayCount',
            type: 'number',
            extraAttrs: 'min="1" max="10"'
        },
        {
            label: '请求最大延迟(ms)',
            name: 'requestMaxDelay',
            type: 'number',
            extraAttrs: 'min="0" max="10000"'
        },
        {
            label: '忽略图片',
            name: 'ignoredImagePattern',
            type: 'textarea',
            extraAttrs: 'rows="5" placeholder="一行输入一个,支持URL模式匹配..."',
            serializeValue: (value) => {
                return value.trim().split('\n').filter(line => line.trim() !== '');
            },
            deserializeValue: (value) => {
                return value ? value.join('\n') : '';
            }
        }
    ];

    const typeHandlers = [
        {
            // 类型名称
            name: "discuz",
            parseArticleElements: () => {
                return document.querySelectorAll('tbody[id^="normalthread_"]');
            },
            parseContentLink: (articleElement) => {
                return articleElement.querySelector('a[onclick="atarget(this)"]')?.href;
            },
            parsePostImage: (link, response) => {
                const images = [];
                const pageContent = new DOMParser().parseFromString(response, 'text/html');
                const postContent = pageContent.querySelector('div[id^="post_"] .plc');
                if (!postContent) {
                    return images;
                }
                const imgElements = postContent.querySelectorAll('img');
                imgElements.forEach(img => {
                    let imageLink = null;
                    imageLink = img.getAttribute('file');
                    if (!imageLink) {
                        imageLink = img.getAttribute('src');
                    }
                    if (imageLink) {
                        images.push(convertPathToAccessible(imageLink, link));
                    }
                });
                return images;
            },
            insertImageContainer: (articleElement, imageContainer) => {
                const tbody = document.createElement("tbody");
                const tr = document.createElement("tr");
                tr.appendChild(imageContainer);
                tbody.appendChild(tr);
                insertElementBelow(articleElement, tbody);
            },
            urlPattern: [
                "*://*/forum-*.html",
                "*://*/forum-*.html?*",
                "*://*/forum.php",
                "*://*/forum.php?*",
                "*://*/*/forum-*.html",
                "*://*/*/forum-*.html?*",
                "*://*/*/forum.php",
                "*://*/*/forum.php?*"
            ],
            ignoredImagePattern: [
                "*://*/*/uc_server/images/*",
                "*://*/*/static/image/*",
                "*://*/*/data/avatar/*"
            ]
        },
        {
            name: "phpwind",
            parseArticleElements: () => {
                return document.querySelectorAll('#ajaxtable tbody:last-of-type tr[align=center]');
            },
            parseContentLink: (articleElement) => {
                return articleElement.querySelector('td a')?.href;
            },
            parsePostImage: (link, response) => {
                const images = [];
                const pageContent = new DOMParser().parseFromString(response, 'text/html');
                const postContent = pageContent.querySelector('.tpc_content');
                if (!postContent) {
                    return images;
                }
                const imgElements = postContent.querySelectorAll('img');
                imgElements.forEach(img => {
                    images.push(convertPathToAccessible(img.src, link));
                });
                return images;
            },
            insertImageContainer: (articleElement, imageContainer) => {
                let tr = document.createElement("tr");
                tr.align = "center";
                let td = document.createElement("td");
                td.colSpan = 5;
                tr.appendChild(td);
                td.appendChild(imageContainer);
                insertElementBelow(articleElement, tr);
            },
            urlPattern: [
                "*://*/*/thread.php",
                "*://*/*/thread.php?*",
                "*://*/thread.php",
                "*://*/thread.php?*"
            ],
            ignoredImagePattern: [
                "*://*/images/post/smile/*",
            ]
        },
        {
            name: "1024",
            parseArticleElements: () => {
                return document.querySelectorAll('tbody[id="tbody"] tr');
            },
            parseContentLink: (articleElement) => {
                return articleElement.querySelector('.tal h3 a')?.href;
            },
            parsePostImage: (link, response) => {
                const images = [];
                const pageContent = new DOMParser().parseFromString(response, 'text/html');
                const postContent = pageContent.querySelector('#conttpc');
                if (!postContent) {
                    return images;
                }
                const imgElements = postContent.querySelectorAll('img');
                imgElements.forEach(img => {
                    let imageLink = null;
                    imageLink = img.getAttribute('ess-data');
                    if (!imageLink) {
                        imageLink = img.getAttribute('src');
                    }
                    if (imageLink) {
                        images.push(convertPathToAccessible(imageLink, link));
                    }
                });
                return images;
            },
            insertImageContainer: (articleElement, imageContainer) => {
                let tr = document.createElement("tr");
                tr.align = "center";
                let td = document.createElement("td");
                td.colSpan = 5;
                tr.appendChild(td);
                td.appendChild(imageContainer);
                insertElementBelow(articleElement, tr);
            },
            urlPattern: [
                "*://*/*/thread0806.php*",
                "*://*/thread0806.php*"
            ],
            ignoredImagePattern: [
            ]
        }
    ];

    function chooseActiveHandler() {
        for (let handler of typeHandlers) {
            if (handler.urlPattern.some(pattern => matchUrl(window.location.href, pattern))) {
                console.log(`激活的配置为:${handler.name}`);
                return handler;
            }
        }
        return null;
    }

    function adjustDefaultSetting(handler) {
        if (handler.ignoredImagePattern) {
            defaultSettings.ignoredImagePattern = handler.ignoredImagePattern;
        }
    }

    function enhancementByHandler(handler, settings) {
        const articleList = handler.parseArticleElements();
        articleList.forEach(element => {
            if (settings.lazyLoad) {
                lazyEnhancement(element, handler, settings);
            } else {
                immediateEnhancement(element, handler, settings);
            }
        })
    }

    function lazyEnhancement(element, handler, settings) {
        window.addEventListener('scroll', throttle(function () {
            const targetElementRect = element.getBoundingClientRect();
            if (targetElementRect.top < window.innerHeight) {
                handleSingleArticle(element, handler, settings);

            }
        }, 200, 500));
    }

    function immediateEnhancement(element, handler, settings) {
        handleSingleArticle(element, handler, settings);
    }

    async function handleSingleArticle(element, handler, settings) {
        if (element.getAttribute("has_enhanced")) {
            return;
        }
        element.setAttribute("has_enhanced", "true");
        let link = handler.parseContentLink(element);
        if (link == null) {
            throw new Error("无法解析文章连接");
        }
        link = convertPathToAccessible(link, window.location.href);
        const articleContent = await httpGetRequest(link, settings.requestMaxDelay);
        if (!articleContent) {
            throw new Error("无法获取文章内容");
        }
        let images = handler.parsePostImage(link, articleContent);
        images = filterArticleImages(images, settings.ignoredImagePattern, settings.maxImageDisplayCount);
        const imageContainer = generateImageContainer(images);
        handler.insertImageContainer(element, imageContainer);
    }

    function generateImageContainer(images) {
        const imageDiv = document.createElement("div");
        imageDiv.style = "display: flex;";
        imageDiv.className = "image_list";
        images.forEach(value => {
            const imgElement = document.createElement("img");
            imgElement.src = value;
            imgElement.style = "max-width: 300px;max-height: 300px;margin-right: 10px"
            imageDiv.appendChild(imgElement);
            imgElement.addEventListener('click', function () {
                var zoomedImg = document.createElement('img');
                zoomedImg.src = imgElement.src;

                zoomedImg.classList.add('zoomable-image', 'zoomed');

                zoomedImg.addEventListener('click', function () {
                    document.body.removeChild(zoomedImg);
                });

                document.body.appendChild(zoomedImg);
            });
        })
        const htmlDivElement = document.createElement("div");
        htmlDivElement.appendChild(imageDiv);
        return htmlDivElement;
    }

    function filterArticleImages(images, ignoredImagePattern, showCount) {
        return images.filter(img => {
            return !ignoredImagePattern.some(pattern => {
                return matchUrl(img, pattern)
            });
        }).slice(0, showCount);
    }

    function convertPathToAccessible(path, currentPath) {
        var url = new URL(path, currentPath);
        return url.href;
    }

    function insertElementBelow(targetElement, newElement) {
        var parentElement = targetElement.parentNode;
        parentElement.insertBefore(newElement, targetElement.nextSibling);
    }



    function httpGetRequest(url, maxDelay) {
        return new Promise((resolve, reject) => {
            const delay = Math.random() * maxDelay;
            setTimeout(() => {
                GM_xmlhttpRequest({
                    method: 'GET',
                    url: url,
                    onload: function (response) {
                        resolve(response.responseText);
                    },
                    onerror: function (error) {
                        reject(error);
                    }
                });
            }, delay);
        });
    }



    function matchUrl(url, pattern) {
        if (typeof url !== 'string' || typeof pattern !== 'string' || !pattern) {
            return false;
        }

        // 解析URL
        let parsedUrl;
        try {
            const urlObj = new URL(url);
            parsedUrl = {
                protocol: urlObj.protocol,
                domain: urlObj.hostname,
                path: urlObj.pathname + urlObj.search + urlObj.hash
            };
        } catch (e) {
            return false; // URL解析失败
        }

        // 验证模式格式
        if (!/^([*]|https?):\/\//.test(pattern)) {
            return false;
        }

        // 转换模式为正则表达式
        let regexStr = pattern
            .replace(/[.+?^${}()|[\]\\]/g, '\\$&') // 转义正则特殊字符
            .replace(/\*/g, '.*?') // 将*替换为非贪婪匹配
            .replace(/^(\*):\/\//, '(http|https):\/\/'); // 处理*://的情况

        // 创建正则表达式并添加锚点
        const regex = new RegExp(`^${regexStr}$`);

        // 组合URL各部分并执行匹配
        const fullUrl = parsedUrl.protocol + '//' + parsedUrl.domain + parsedUrl.path;
        return regex.test(fullUrl);
    }

    /**
     * 节流
     * @param func {function} 回调函数
     * @param wait 延迟执行时间(ms)
     * @param mustRun 必须执行时间(ms)
     * @returns {(function(): void)|*}
     */
    function throttle(func, wait, mustRun) {
        var timeout,
            startTime = new Date();

        return function () {
            var context = this,
                args = arguments,
                curTime = new Date();

            clearTimeout(timeout);
            // 如果达到了规定的触发时间间隔,触发 handler
            if (curTime - startTime >= mustRun) {
                func.apply(context, args);
                startTime = curTime;
                // 没达到触发间隔,重新设定定时器
            } else {
                timeout = setTimeout(func, wait);
            }
        };
    };

    // 初始化设置
    function initSettings() {
        const saveSettings = GM_getValue(getSettingName()) ?? {};
        settingsItems.forEach(item => {
            const savedValue = saveSettings[item.name];
            currentSettings[item.name] = savedValue !== undefined ? savedValue : defaultSettings[item.name];
        });
        console.log(`当前脚本设置:${JSON.stringify(currentSettings)}`);
    }

    function getSettingName() {
        return window.location.host + '_settings';
    }

    // 创建设置项(支持所有类型)
    function createSettingItem(labelText, name, type, value, extraAttrs = '', options = []) {
        const container = document.createElement('div');
        container.style.marginBottom = '1.5rem';

        const label = document.createElement('label');
        label.style.display = 'block';
        label.style.marginBottom = '8px';
        label.style.fontWeight = '500';
        label.textContent = labelText;
        container.appendChild(label);

        switch (type) {
            case 'checkbox-group': {
                const group = document.createElement('div');
                options.forEach(option => {
                    const wrapper = document.createElement('div');
                    wrapper.style.marginBottom = '6px';

                    const input = document.createElement('input');
                    input.type = 'checkbox';
                    input.name = `${name}[${option.value}]`;
                    input.id = `${name}_${option.value}`;
                    input.checked = value[option.value] === true;
                    input.style.marginRight = '8px';

                    const optLabel = document.createElement('label');
                    optLabel.htmlFor = `${name}_${option.value}`;
                    optLabel.textContent = option.text;

                    wrapper.append(input, optLabel);
                    group.appendChild(wrapper);
                });
                container.appendChild(group);
                break;
            }

            case 'radio-group': {
                const group = document.createElement('div');
                options.forEach(option => {
                    const wrapper = document.createElement('div');
                    wrapper.style.marginBottom = '6px';

                    const input = document.createElement('input');
                    input.type = 'radio';
                    input.name = name;
                    input.id = `${name}_${option.value}`;
                    input.value = option.value;
                    input.checked = value === option.value;
                    input.style.marginRight = '8px';

                    const optLabel = document.createElement('label');
                    optLabel.htmlFor = `${name}_${option.value}`;
                    optLabel.textContent = option.text;

                    wrapper.append(input, optLabel);
                    group.appendChild(wrapper);
                });
                container.appendChild(group);
                break;
            }

            case 'select': {
                const select = document.createElement('select');
                options.forEach(option => {
                    const opt = document.createElement('option');
                    opt.value = option.value;
                    opt.textContent = option.text;
                    opt.selected = option.value === value;
                    select.appendChild(opt);
                });
                select.name = name;
                select.id = name;
                select.style.width = '100%';
                select.style.padding = '6px';
                select.style.border = '1px solid #ddd';
                select.style.borderRadius = '4px';
                container.appendChild(select);
                break;
            }

            case 'textarea': {
                const textarea = document.createElement('textarea');
                textarea.name = name;
                textarea.id = name;
                textarea.value = value;
                applyExtraAttrs(textarea, extraAttrs);
                textarea.style.width = '100%';
                textarea.style.padding = '6px';
                textarea.style.border = '1px solid #ddd';
                textarea.style.borderRadius = '4px';
                container.appendChild(textarea);
                break;
            }

            default: {
                // 处理所有input类型
                const input = document.createElement('input');
                input.type = type;
                input.name = name;
                input.id = name;

                if (type === 'checkbox') {
                    input.checked = value;
                } else {
                    input.value = value;
                }

                applyExtraAttrs(input, extraAttrs);

                // 滑块特殊处理
                if (type === 'range') {
                    const wrapper = document.createElement('div');
                    wrapper.style.display = 'flex';
                    wrapper.style.alignItems = 'center';

                    input.style.width = '80%';
                    input.style.marginRight = '10px';

                    const valueDisplay = document.createElement('span');
                    valueDisplay.textContent = value;
                    valueDisplay.style.width = '20%';
                    valueDisplay.style.textAlign = 'center';

                    input.addEventListener('input', () => {
                        valueDisplay.textContent = input.value;
                    });

                    wrapper.append(input, valueDisplay);
                    container.appendChild(wrapper);
                } else {
                    input.style.width = '100%';
                    input.style.padding = '6px';
                    input.style.border = '1px solid #ddd';
                    input.style.borderRadius = '4px';

                    if (type === 'checkbox') {
                        input.style.width = 'auto';
                        input.style.marginRight = '8px';
                        const wrapper = document.createElement('div');
                        wrapper.style.display = 'flex';
                        wrapper.style.alignItems = 'center';
                        wrapper.append(input, label);
                        container.innerHTML = '';
                        container.appendChild(wrapper);
                    } else {
                        container.appendChild(input);
                    }
                }
            }
        }

        return container;
    }

    // 应用额外属性
    function applyExtraAttrs(element, attrsStr) {
        if (!attrsStr) return;
        attrsStr.split(' ').forEach(attr => {
            const [key, val] = attr.split('=');
            if (key && val) {
                element.setAttribute(key, val.replace(/"/g, ''));
            }
        });
    }


    function showAlert(type, message, closeTime = 0) {
        // 类型样式映射
        const styleMap = {
            success: { bg: '#4CAF50', icon: '✓' },
            error: { bg: '#F44336', icon: '✕' },
            warning: { bg: '#FFC107', icon: '!' }
        };

        // 默认使用警告样式
        const style = styleMap[type] || styleMap.warning;

        // 创建提示框元素
        const alertDiv = document.createElement('div');
        alertDiv.style.cssText = `
        position: fixed;
        top: 20px;
        left: 50%;
        transform: translateX(-50%);
        padding: 15px 25px;
        background: ${style.bg};
        color: white;
        border-radius: 4px;
        box-shadow: 0 2px 15px rgba(0,0,0,0.3);
        z-index: 999999;
        display: flex;
        align-items: center;
        gap: 12px;
        font-family: Arial, sans-serif;
        animation: slideDown 0.3s ease-out;
    `;

        // 构建内容(根据是否自动关闭决定是否显示关闭按钮)
        let content = `<span style="font-weight: bold; font-size: 1.2em;">${style.icon}</span>
                   <span>${message}</span>`;

        // 当不自动关闭时才显示关闭按钮
        if (closeTime <= 0) {
            content += `<button style="margin-left: 15px; background: rgba(255,255,255,0.3); border: none; color: white; border-radius: 50%; width: 20px; height: 20px; cursor: pointer; transition: background 0.2s;">×</button>`;
        }

        alertDiv.innerHTML = content;

        // 添加到页面
        document.body.appendChild(alertDiv);

        // 关闭按钮事件(仅当存在关闭按钮时)
        const closeBtn = alertDiv.querySelector('button');
        if (closeBtn) {
            closeBtn.addEventListener('click', () => closeAlert(alertDiv));
            // 按钮悬停效果
            closeBtn.addEventListener('mouseover', () => {
                closeBtn.style.background = 'rgba(255,255,255,0.5)';
            });
            closeBtn.addEventListener('mouseout', () => {
                closeBtn.style.background = 'rgba(255,255,255,0.3)';
            });
        }

        // 自动关闭功能
        if (closeTime > 0) {
            setTimeout(() => closeAlert(alertDiv), closeTime);
        }

        if (!document.getElementById('custom-alert-animations')) {
            const styleSheet = document.createElement('style');
            styleSheet.id = 'custom-alert-animations';
            styleSheet.textContent = `
            @keyframes slideDown {
                from { transform: translateX(-50%) translateY(-20px); opacity: 0; }
                to { transform: translateX(-50%) translateY(0); opacity: 1; }
            }
        `;
            document.head.appendChild(styleSheet);
        }
    }

    function closeAlert(alertDiv) {
        alertDiv.style.opacity = '0';
        alertDiv.style.transform = 'translateX(-50%) translateY(-20px)';
        setTimeout(() => alertDiv.remove(), 300);
    }

    // 创建设置菜单
    function createSettingsMenu() {
        let menu = document.getElementById('tampermonkey-settings-menu');
        if (menu) return menu;

        menu = document.createElement('div');
        menu.id = 'tampermonkey-settings-menu';
        menu.style.cssText = `
        position: fixed;
        top: 50%;
        left: 50%;
        transform: translate(-50%, -50%);
        background: white;
        padding: 1.5rem;
        border-radius: 8px;
        box-shadow: 0 4px 20px rgba(0,0,0,0.15);
        z-index: 9999;
        min-width: 300px;
        max-width: 90%;
        max-height: 80vh;
        overflow-y: auto;
        display: none;
    `;

        const title = document.createElement('h3');
        title.textContent = '脚本设置';
        title.style.cssText = `margin: 0 0 1.5rem 0; text-align: center; padding-bottom: 0.5rem; border-bottom: 1px solid #eee;`;
        menu.appendChild(title);

        const form = document.createElement('form');
        form.id = 'settings-form';
        settingsItems.forEach(item => {
            const saveValue = currentSettings[item.name];
            const parseValue = item.deserializeValue ? item.deserializeValue(saveValue) : saveValue;
            form.appendChild(createSettingItem(
                item.label,
                item.name,
                item.type,
                parseValue,
                item.extraAttrs,
                item.options
            ));
        });

        // 创建按钮容器,使按钮同排显示
        const buttonContainer = document.createElement('div');
        buttonContainer.style.cssText = `
        display: flex;
        gap: 10px;
        margin-top: 1rem;
    `;

        // 保存按钮
        const saveBtn = document.createElement('button');
        saveBtn.type = 'submit';
        saveBtn.textContent = '保存';
        saveBtn.style.cssText = `
        flex: 1;
        padding: 8px;
        background: #007bff;
        color: white;
        border: none;
        border-radius: 4px;
        cursor: pointer;
    `;
        buttonContainer.appendChild(saveBtn);

        // 重置按钮
        const resetBtn = document.createElement('button');
        resetBtn.type = 'button';
        resetBtn.textContent = '重置';
        resetBtn.style.cssText = `
        flex: 1;
        padding: 8px;
        background: #ffc107;
        color: #212529;
        border: none;
        border-radius: 4px;
        cursor: pointer;
    `;
        resetBtn.addEventListener('click', (e) => {
            if (resetToDefault()) {
                hideMenu();
            }
        });
        buttonContainer.appendChild(resetBtn);

        // 关闭按钮
        const closeBtn = document.createElement('button');
        closeBtn.type = 'button';
        closeBtn.textContent = '关闭';
        closeBtn.style.cssText = `
        flex: 1;
        padding: 8px;
        background: #6c757d;
        color: white;
        border: none;
        border-radius: 4px;
        cursor: pointer;
    `;
        closeBtn.addEventListener('click', hideMenu);
        buttonContainer.appendChild(closeBtn);

        form.appendChild(buttonContainer);
        menu.appendChild(form);
        document.body.appendChild(menu);

        form.addEventListener('submit', (e) => {
            e.preventDefault();
            if (saveSettings()) {
                hideMenu();
            };
        });

        return menu;
    }

    function resetToDefault() {
        const confirmReset = confirm('确定要将所有设置恢复到默认值吗?此操作不可撤销。');

        if (!confirmReset) {
            return false;
        }
        GM_deleteValue(getSettingName());
        initSettings();
        showAlert('success', '设置已重置', 3000);
        return true;
    }


    // 保存设置
    function saveSettings() {
        const form = document.getElementById('settings-form');
        const toSaveSettings = {};
        for (const key in settingsItems) {
            const item = settingsItems[key];
            let rawValue;

            switch (item.type) {
                case 'checkbox-group':
                    rawValue = [];
                    item.options.forEach(option => {
                        if (form.elements[`${item.name}[${option.value}]`]?.checked) {
                            rawValue.push(option.value);
                        }
                    });
                    break;
                case 'radio-group':
                    rawValue = form.elements[item.name].value;
                    break;
                case 'checkbox':
                    rawValue = form.elements[item.name].checked;
                    break;
                default:
                    rawValue = form.elements[item.name].value;
            }
            if (item.validValue) {
                const msg = item.validValue(rawValue);
                if (msg) {
                    showAlert('warning', msg, 5000);
                    return false;
                }
            }
            const saveValue = item.serializeValue ? item.serializeValue(rawValue) : rawValue;
            toSaveSettings[item.name] = saveValue;
        }
        Object.keys(toSaveSettings).forEach(key => {
            currentSettings[key] = toSaveSettings[key];
        });
        GM_setValue(getSettingName(), currentSettings);
        showAlert('success', '设置已保存', 3000);
        return true;
    }

    // 显示/隐藏菜单
    function showMenu() {
        createSettingsMenu().style.display = 'block';
    }

    function hideMenu() {
        document.getElementById('tampermonkey-settings-menu')?.remove();
    }

    // 初始化
    const handler = chooseActiveHandler();
    if (handler == null) {
        return;
    }
    adjustDefaultSetting(handler);
    initSettings();
    GM_registerMenuCommand('脚本设置', showMenu, 's');
    if (!currentSettings.enabled) {
        return;
    }
    enhancementByHandler(handler, currentSettings);
})();