没有朋友

移除页面aff参数,并为代码块增加一键复制按钮。

当前为 2025-08-10 提交的版本,查看 最新版本

// ==UserScript==
// @name         没有朋友
// @namespace    https://linux.do/u/amna/
// @version      2.0
// @description  移除页面aff参数,并为代码块增加一键复制按钮。
// @license      MIT
// @author       https://linux.do/u/amna/
// @match        *://ygpy.net/*
// @run-at       document-end
// @grant        GM_setClipboard
// @grant        GM_addStyle
// ==/UserScript==

(function() {
    'use strict';

    // --- 全局常量和配置 ---
    // 【重构】将常量集中管理,便于维护。
    const SCRIPT_BUTTON_CLASS = 'amna-copy-button'; // 使用更具辨识度的类名
    const DEBOUNCE_DELAY = 250; //ms

    /**
     * @description 需要忽略的文本内容,不为其添加复制按钮。
     * @type {Set<string>}
     */
    const IGNORED_CODE_TEXT = new Set([
        "流媒体解锁", "多设备在线", "支持 BT 下载", "国内中转线路", "IEPL 专线路", "设备不限制",
        "晚高峰稳定", "国际专线", "IPLC 线路", "50+ 节点", "不限设备", "超大带宽", "赠送 Emby",
        "IEPL 专线", "家宽 IP", "厦门专线", "开始阅读", "仪表盘", "主页", "兑换礼品卡"
    ]);

    // --- 工具函数 ---

    /**
     * @description 防抖函数,用于延迟执行,合并高频触发的事件。
     * @param {Function} func - 需要防抖的函数。
     * @param {number} wait - 延迟的毫秒数。
     * @returns {Function} - 包装后的防抖函数。
     */
    function debounce(func, wait) {
        let timeout;
        return function(...args) {
            clearTimeout(timeout);
            timeout = setTimeout(() => func.apply(this, args), wait);
        };
    }

    /**
     * @description 惰性注入CSS样式,确保只执行一次。
     * @returns {void}
     */
    const injectStyles = (() => {
        let injected = false;
        return () => {
            if (injected) return;
            GM_addStyle(`
                .${SCRIPT_BUTTON_CLASS} {
                    margin-left: 8px;
                    padding: 3px 6px;
                    border: 1px solid #ccc;
                    border-radius: 4px;
                    background-color: #f0f0f0;
                    color: #333;
                    cursor: pointer;
                    font-size: 12px;
                    font-family: sans-serif;
                    user-select: none;
                    display: inline-block;
                    vertical-align: middle;
                    opacity: 0.7;
                    transition: opacity 0.2s, background-color 0.2s;
                }
                .${SCRIPT_BUTTON_CLASS}:hover {
                    background-color: #e0e0e0;
                    border-color: #bbb;
                    opacity: 1;
                }
                .${SCRIPT_BUTTON_CLASS}:active {
                    background-color: #d0d0d0;
                }
            `);
            injected = true;
        };
    })();


    // --- 核心功能模块 ---

    /**
     * @description 净化指定容器内的链接,移除查询参数(包括在hash中的参数)。
     * @param {HTMLElement} container - 要扫描的DOM元素。
     */
    function cleanLinks(container) {
        const links = container.querySelectorAll('a[href*="?"]');
        links.forEach(link => {
            // 只处理有 href 属性的有效链接
            if (!link.href) return;

            try {
                // 使用 link.href 可以获取到完整的绝对路径 URL
                const url = new URL(link.href);

                const hasSearchParams = url.search;
                const hasHashParams = url.hash && url.hash.includes('?');

                // 【关键修复】只有在 search 或 hash 中存在查询参数时才进行处理
                if (hasSearchParams || hasHashParams) {
                    let newHref = url.origin + url.pathname;
                    if (hasHashParams) {
                        newHref += url.hash.split('?')[0];
                    }
                    link.href = newHref;
                }
            } catch (error) {
                // 对于无效URL(如 "javascript:;"),URL构造函数会抛出异常,这里进行降级处理。
                // 这个简单的字符串分割可以同时处理标准查询和哈希内查询。
                if (link.href.includes('?')) {
                    link.href = link.href.split('?')[0];
                }
            }
        });
    }

    /**
     * @description 为指定容器内的 <code> 标签添加复制按钮。
     * @param {HTMLElement} container - 要扫描的DOM元素。
     */
    function addCopyButtons(container) {
        injectStyles(); // 确保样式已注入

        // 【优化】查询时直接排除已经处理过的(即内部有我们按钮兄弟的)code的父元素
        const codeBlocks = container.querySelectorAll('code:not([data-amna-processed])');

        codeBlocks.forEach(code => {
            code.setAttribute('data-amna-processed', 'true'); // 【优化】使用 data 属性标记,比检查兄弟节点更可靠

            const textContent = code.textContent?.trim();
            if (!textContent || IGNORED_CODE_TEXT.has(textContent)) {
                return;
            }

            // 【重构】使用更可靠的标记来避免重复添加
            if (code.nextElementSibling?.classList.contains(SCRIPT_BUTTON_CLASS)) {
                return;
            }

            const copyButton = document.createElement('button');
            copyButton.textContent = '复制';
            copyButton.className = SCRIPT_BUTTON_CLASS;

            copyButton.addEventListener('click', (e) => {
                e.preventDefault();
                e.stopPropagation();

                GM_setClipboard(textContent, 'text');

                copyButton.textContent = '已复制!';
                copyButton.disabled = true;
                setTimeout(() => {
                    copyButton.textContent = '复制';
                    copyButton.disabled = false;
                }, 2000);
            });

            // 【优化】使用 .after() 方法,代码更简洁
            code.after(copyButton);
        });
    }

    // --- 主执行逻辑与DOM监控 ---

    /**
     * @description 统一处理节点,应用所有功能。
     * @param {Node} node - 要处理的DOM节点, 通常是 document.body 或动态添加的元素。
     */
    function processNode(node) {
        // 确保是元素节点
        if (!(node instanceof Element)) return;

        cleanLinks(node);
        addCopyButtons(node);
    }

    // 防抖包装的主处理函数
    const debouncedProcessAll = debounce(() => {
        console.log('[没有朋友] DOM发生变化,重新扫描页面...');
        processNode(document.body);
    }, DEBOUNCE_DELAY);

    // 启动脚本
    function main() {
        console.log('[没有朋友] 脚本已启动。');
        // 1. 立即处理页面现有内容
        processNode(document.body);

        // 2. 监听未来的DOM变化
        const observer = new MutationObserver((mutations) => {
            // 【优化】对 mutations 进行更精细的检查,只关心添加的元素节点
            for (const mutation of mutations) {
                if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
                    // 只要有节点添加,就触发防抖检查,无需检查是否是自己的按钮,
                    // 因为我们的 addCopyButtons 函数本身是幂等的。
                    debouncedProcessAll();
                    return; // 一旦发现变化,即可退出循环并等待防抖函数执行
                }
            }
        });

        observer.observe(document.body, {
            childList: true,
            subtree: true
        });
    }

    // 确保在 DOM 完全加载后执行
    if (document.readyState === 'loading') {
        window.addEventListener('DOMContentLoaded', main);
    } else {
        main();
    }

})();