Greasy Fork

来自缓存

Greasy Fork is available in English.

网站信息复制助手

修复点击穿透bug、增加隐藏按钮选项。功能:修复间隙、深色模式、位置记忆、触控拖拽、自动解码、自定义快捷键、Markdown/HTML切换。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         网站信息复制助手
// @namespace    http://tampermonkey.net/
// @version      0.14
// @description  修复点击穿透bug、增加隐藏按钮选项。功能:修复间隙、深色模式、位置记忆、触控拖拽、自动解码、自定义快捷键、Markdown/HTML切换。
// @author       Gibber1977
// @match        *://*/*
// @grant        GM_addStyle
// @grant        GM_setClipboard
// @grant        GM_registerMenuCommand
// @grant        GM_unregisterMenuCommand
// @grant        GM_getValue
// @grant        GM_setValue
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    // --- 1. 核心配置与状态管理 ---
    const CONFIG = {
        menuWidth: 200,
        gapSize: 10,
        defaultType: 'clean',
        cleanParams: [
            'spm_id_from', 'vd_source', 'share_source', 'share_medium', 'share_plat', 'share_tag', 'unique_k',
            'utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'utm_id',
            'fbclid', 'gclid', 'si', 'feature', 'pp', 'biz_id', 'scene', 'isappinstalled', 'igshid',
            'sc_medium', 'sc_source', 'sc_campaign', 'ref', 'source'
        ]
    };

    const STATE = {
        isDragging: false,
        hasMoved: false,
        startX: 0,
        startY: 0,
        initialLeft: 0,
        initialTop: 0,
        isFabVisible: GM_getValue('show_fab', true) // 读取显示状态
    };

    // --- 2. 菜单命令逻辑 (动态注册) ---
    let menuCmdId = null;
    function updateMenuCommand() {
        if (menuCmdId !== null) {
            GM_unregisterMenuCommand(menuCmdId);
        }
        const title = STATE.isFabVisible ? '🚫 隐藏悬浮球' : '👁️ 显示悬浮球';
        menuCmdId = GM_registerMenuCommand(title, () => {
            STATE.isFabVisible = !STATE.isFabVisible;
            GM_setValue('show_fab', STATE.isFabVisible);
            updateFabVisibility();
            updateMenuCommand();
            // 如果隐藏时菜单还开着,关掉它
            if (!STATE.isFabVisible) {
                closeMenu();
            }
        });
    }

    // 设置快捷键命令
    GM_registerMenuCommand('⚙️ 设置复制快捷键', () => {
        const current = GM_getValue('user_shortcut', '') || '未设置';
        const input = prompt(
            `请输入快捷键组合 (使用 + 号连接,不区分大小写)\n例如: alt+c 或 ctrl+shift+z\n\n当前: ${current}\n留空确认则禁用。`,
            GM_getValue('user_shortcut', '')
        );
        if (input !== null) {
            GM_setValue('user_shortcut', input.trim().toLowerCase());
            alert(input ? `✅ 快捷键已更新: ${input}` : '🚫 快捷键已禁用');
        }
    });

    // 初始化菜单
    updateMenuCommand();

    // --- 3. DOM 构建 (Shadow DOM) ---
    const host = document.createElement('div');
    host.id = 'copy-helper-host';
    // pointer-events: none 确保 host 本身不阻挡点击,内部元素开启 auto
    host.style.cssText = 'position: fixed; z-index: 2147483647; top: 0; left: 0; width: 0; height: 0; pointer-events: none;';
    document.body.appendChild(host);
    const shadow = host.attachShadow({ mode: 'open' });

    // --- 4. 样式系统 ---
    const style = document.createElement('style');
    style.textContent = `
        :host {
            --primary: #00A1D6;
            --text: #333;
            --text-sub: #888;
            --bg: rgba(255, 255, 255, 0.95);
            --border: #eaeaea;
            --shadow: 0 4px 20px rgba(0,0,0,0.15);
            --hover-bg: #f4f9ff;
            --fab-bg: #fff;
            --fab-color: #555;
            --toast-bg: rgba(30, 30, 30, 0.9);
        }

        @media (prefers-color-scheme: dark) {
            :host {
                --primary: #5ec7f7;
                --text: #e0e0e0;
                --text-sub: #aaa;
                --bg: rgba(35, 35, 35, 0.95);
                --border: #444;
                --shadow: 0 4px 24px rgba(0,0,0,0.6);
                --hover-bg: #444;
                --fab-bg: #2d2d2d;
                --fab-color: #ddd;
                --toast-bg: rgba(255, 255, 255, 0.9);
            }
        }

        * { box-sizing: border-box; user-select: none; -webkit-user-select: none; }

        /* 全屏透明遮罩 (解决点击穿透) */
        #overlay {
            position: fixed; top: 0; left: 0; width: 100vw; height: 100vh;
            z-index: 99; /* 比 FAB 和 Menu 低,但覆盖网页 */
            display: none;
            pointer-events: auto; /* 捕获点击 */
        }
        #overlay.active { display: block; }

        /* 悬浮球 FAB */
        #fab {
            position: fixed; width: 44px; height: 44px;
            background: var(--fab-bg); border: 1px solid var(--border);
            border-radius: 50%; box-shadow: var(--shadow);
            cursor: pointer; display: flex; align-items: center; justify-content: center;
            transition: transform 0.1s, box-shadow 0.2s, opacity 0.3s;
            backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px);
            touch-action: none;
            z-index: 100;
            pointer-events: auto; /* 恢复点击 */
        }
        #fab:hover { transform: scale(1.1); box-shadow: 0 8px 25px rgba(0,0,0,0.2); }
        #fab:active { transform: scale(0.95); }
        #fab svg { width: 22px; height: 22px; fill: var(--fab-color); transition: fill 0.3s; }

        #fab.idle { opacity: 0.6; transform: scale(0.9); }
        #fab.idle:hover { opacity: 1; transform: scale(1.1); }
        #fab.hidden { display: none !important; }

        /* 菜单 Menu */
        #menu {
            position: fixed; width: ${CONFIG.menuWidth}px;
            background: var(--bg); border: 1px solid var(--border);
            border-radius: 12px; padding: 8px 0;
            box-shadow: var(--shadow); display: none; flex-direction: column;
            font-family: system-ui, sans-serif; font-size: 13px; color: var(--text);
            overflow: visible; opacity: 0; transform: scale(0.95);
            transition: opacity 0.15s, transform 0.15s;
            z-index: 101; /* 最高层级 */
            pointer-events: auto;
        }
        #menu.visible { display: flex; opacity: 1; transform: scale(1); }

        .menu-item {
            position: relative; padding: 10px 16px; cursor: pointer;
            display: flex; justify-content: space-between; align-items: center;
        }
        .menu-item:hover { background: var(--hover-bg); color: var(--primary); }

        .divider { height: 1px; background: var(--border); margin: 5px 0; opacity: 0.6; }
        .hint { font-size: 11px; color: var(--text-sub); margin-left: 6px; font-weight: normal; }
        .menu-item:hover .hint { color: var(--primary); opacity: 0.8; }

        .submenu {
            position: absolute; top: -8px; width: ${CONFIG.menuWidth}px;
            background: var(--bg); border: 1px solid var(--border);
            border-radius: 12px; box-shadow: var(--shadow);
            padding: 8px 0; display: none; z-index: 102;
        }
        .submenu::before {
            content: ''; position: absolute; top: 0; bottom: 0;
            width: ${CONFIG.gapSize + 15}px; z-index: -1;
        }
        .opens-right .submenu { left: 100%; margin-left: ${CONFIG.gapSize}px; }
        .opens-right .submenu::before { right: 100%; }
        .opens-left .submenu { right: 100%; margin-right: ${CONFIG.gapSize}px; }
        .opens-left .submenu::before { left: 100%; }

        .menu-item:hover .submenu { display: block; animation: slideIn 0.15s ease-out; }

        #toast {
            position: fixed; top: 50%; left: 50%;
            transform: translate(-50%, -50%) scale(0.9);
            background: var(--toast-bg); color: #fff;
            padding: 12px 28px; border-radius: 30px;
            font-size: 14px; font-weight: 500;
            opacity: 0; pointer-events: none;
            transition: all 0.3s cubic-bezier(0.2, 0.8, 0.2, 1);
            box-shadow: 0 10px 30px rgba(0,0,0,0.3);
            display: flex; align-items: center; gap: 8px;
            z-index: 2147483647;
            backdrop-filter: blur(10px);
        }
        @media (prefers-color-scheme: dark) { #toast { color: #000; } }

        #toast.show { opacity: 1; transform: translate(-50%, -50%) scale(1); }

        @keyframes slideIn { from { opacity: 0; transform: translateX(5px); } to { opacity: 1; transform: translateX(0); } }
        @keyframes pulse { 0% { box-shadow: 0 0 0 0 var(--primary); } 70% { box-shadow: 0 0 0 10px rgba(0,0,0,0); } 100% { box-shadow: 0 0 0 0 rgba(0,0,0,0); } }
        .pulse { animation: pulse 0.5s; }
    `;
    shadow.appendChild(style);

    // --- 5. 元素初始化 ---

    // 遮罩层 (新增)
    const overlay = document.createElement('div');
    overlay.id = 'overlay';

    const fab = document.createElement('div');
    fab.id = 'fab';
    fab.innerHTML = `<svg viewBox="0 0 24 24"><path d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"/></svg>`;

    const savedX = GM_getValue('fab_pos_x', window.innerWidth - 70);
    const savedY = GM_getValue('fab_pos_y', window.innerHeight * 0.6);
    const safeX = Math.min(Math.max(0, savedX), window.innerWidth - 44);
    const safeY = Math.min(Math.max(0, savedY), window.innerHeight - 44);

    fab.style.left = safeX + 'px';
    fab.style.top = safeY + 'px';

    // 初始可见性
    function updateFabVisibility() {
        if (STATE.isFabVisible) {
            fab.classList.remove('hidden');
        } else {
            fab.classList.add('hidden');
        }
    }
    updateFabVisibility();

    const menu = document.createElement('div');
    menu.id = 'menu';

    const toast = document.createElement('div');
    toast.id = 'toast';

    shadow.appendChild(overlay); // 先添加遮罩
    shadow.appendChild(fab);
    shadow.appendChild(menu);
    shadow.appendChild(toast);

    // --- 6. 核心功能函数 ---

    function getCleanUrl() {
        try {
            const url = new URL(window.location.href);
            CONFIG.cleanParams.forEach(p => url.searchParams.delete(p));
            return decodeURI(url.href);
        } catch { return window.location.href; }
    }

    async function copyToClipboard(text) {
        try {
            await navigator.clipboard.writeText(text);
            return true;
        } catch (err) {
            try {
                GM_setClipboard(text);
                return true;
            } catch (e) {
                console.error('Copy failed', e);
                return false;
            }
        }
    }

    async function handleCopy(text, label) {
        const success = await copyToClipboard(text);
        if (success) {
            fab.classList.remove('pulse');
            void fab.offsetWidth;
            fab.classList.add('pulse');
            showToast(`✅ 已复制 ${label}`);
            closeMenu();
        } else {
            showToast(`❌ 复制失败,请手动复制`);
        }
    }

    function showToast(msg) {
        toast.textContent = msg;
        toast.classList.add('show');
        clearTimeout(toast.timer);
        toast.timer = setTimeout(() => toast.classList.remove('show'), 2000);
    }

    function closeMenu() {
        menu.classList.remove('visible');
        overlay.classList.remove('active'); // 隐藏遮罩
    }

    // --- 7. 菜单渲染与数据 ---
    function getMenuData() {
        const title = document.title.trim();
        const cleanLink = getCleanUrl();
        const origLink = decodeURI(window.location.href);
        const isCleanDefault = CONFIG.defaultType === 'clean';

        return [
            { label: '📝 仅标题', action: () => handleCopy(title, '标题') },
            { type: 'divider' },
            {
                label: `🔗 链接 <span class="hint">${isCleanDefault ? '净化' : '原始'}</span>`,
                action: () => handleCopy(isCleanDefault ? cleanLink : origLink, '链接'),
                children: [
                    { label: '✨ 净化链接', action: () => handleCopy(cleanLink, '净化链接') },
                    { label: '🌍 原始链接', action: () => handleCopy(origLink, '原始链接') }
                ]
            },
            {
                label: `📌 标题+链接`,
                action: () => handleCopy(`${title}\n${isCleanDefault ? cleanLink : origLink}`, '标题+链接'),
                children: [
                    { label: '✨ 标题 + 净化', action: () => handleCopy(`${title}\n${cleanLink}`, '标题+净化链接') },
                    { label: '🌍 标题 + 原始', action: () => handleCopy(`${title}\n${origLink}`, '标题+原始链接') }
                ]
            },
            {
                label: `📦 Markdown`,
                action: () => handleCopy(`[${title}](${isCleanDefault ? cleanLink : origLink})`, 'Markdown'),
                children: [
                    { label: '✨ Markdown (净化)', action: () => handleCopy(`[${title}](${cleanLink})`, 'Markdown净化') },
                    { label: '🌍 Markdown (原始)', action: () => handleCopy(`[${title}](${origLink})`, 'Markdown原始') }
                ]
            },
            {
                label: `💻 HTML`,
                action: () => handleCopy(`<a href="${isCleanDefault ? cleanLink : origLink}">${title}</a>`, 'HTML'),
                children: [
                    { label: '✨ HTML (净化)', action: () => handleCopy(`<a href="${cleanLink}">${title}</a>`, 'HTML净化') },
                    { label: '🌍 HTML (原始)', action: () => handleCopy(`<a href="${origLink}">${title}</a>`, 'HTML原始') }
                ]
            }
        ];
    }

    function renderMenu() {
        menu.innerHTML = '';
        getMenuData().forEach(item => {
            if (item.type === 'divider') {
                menu.appendChild(document.createElement('div')).className = 'divider';
                return;
            }
            const div = document.createElement('div');
            div.className = 'menu-item';
            div.innerHTML = `<span>${item.label}</span>${item.children ? '<svg viewBox="0 0 24 24" style="width:14px;opacity:0.5;fill:currentColor"><path d="M10 17l5-5-5-5v10z"/></svg>' : ''}`;

            div.addEventListener('click', (e) => {
                if(item.action) { item.action(); e.stopPropagation(); }
            });

            if (item.children) {
                const subDiv = document.createElement('div');
                subDiv.className = 'submenu';
                item.children.forEach(sub => {
                    const subItem = document.createElement('div');
                    subItem.className = 'menu-item';
                    subItem.textContent = sub.label.replace(/✨|🌍|📝|🔗|📌|📦|💻/g, '').trim();
                    subItem.addEventListener('click', (ev) => {
                        ev.stopPropagation();
                        sub.action();
                    });
                    subDiv.appendChild(subItem);
                });
                div.appendChild(subDiv);
            }
            menu.appendChild(div);
        });
    }

    // --- 8. 交互系统 ---

    const getClientPos = (e) => {
        const touch = e.touches ? e.touches[0] : e;
        return { x: touch.clientX, y: touch.clientY };
    };

    const handleStart = (e) => {
        if (e.type === 'mousedown' && e.button !== 0) return;

        STATE.isDragging = true;
        STATE.hasMoved = false;
        const pos = getClientPos(e);
        STATE.startX = pos.x;
        STATE.startY = pos.y;

        const rect = fab.getBoundingClientRect();
        STATE.initialLeft = rect.left;
        STATE.initialTop = rect.top;

        fab.style.transition = 'none';
        fab.classList.remove('idle');

        if(e.type === 'touchstart') e.preventDefault();
    };

    const handleMove = (e) => {
        if (!STATE.isDragging) return;

        const pos = getClientPos(e);
        const dx = pos.x - STATE.startX;
        const dy = pos.y - STATE.startY;

        if (dx*dx + dy*dy > 25) STATE.hasMoved = true;

        const maxLeft = window.innerWidth - fab.offsetWidth;
        const maxTop = window.innerHeight - fab.offsetHeight;

        const newLeft = Math.min(Math.max(0, STATE.initialLeft + dx), maxLeft);
        const newTop = Math.min(Math.max(0, STATE.initialTop + dy), maxTop);

        fab.style.left = newLeft + 'px';
        fab.style.top = newTop + 'px';

        if (menu.classList.contains('visible')) closeMenu();
    };

    const handleEnd = () => {
        if (!STATE.isDragging) return;
        STATE.isDragging = false;

        fab.style.transition = 'all 0.3s cubic-bezier(0.18, 0.89, 0.32, 1.28)';

        const rect = fab.getBoundingClientRect();
        const centerX = rect.left + rect.width / 2;

        let finalLeft;
        if (centerX < window.innerWidth / 2) {
            finalLeft = 10;
        } else {
            finalLeft = window.innerWidth - fab.offsetWidth - 10;
        }

        fab.style.left = finalLeft + 'px';
        GM_setValue('fab_pos_x', finalLeft);
        GM_setValue('fab_pos_y', rect.top);
        resetIdleTimer();
    };

    fab.addEventListener('mousedown', handleStart);
    fab.addEventListener('touchstart', handleStart, { passive: false });
    document.addEventListener('mousemove', handleMove);
    document.addEventListener('touchmove', handleMove, { passive: false });
    document.addEventListener('mouseup', handleEnd);
    document.addEventListener('touchend', handleEnd);

    let idleTimer;
    const resetIdleTimer = () => {
        fab.classList.remove('idle');
        clearTimeout(idleTimer);
        idleTimer = setTimeout(() => {
            if(!menu.classList.contains('visible')) fab.classList.add('idle');
        }, 3000);
    };
    fab.addEventListener('mouseenter', resetIdleTimer);
    resetIdleTimer();

    // 点击展开菜单
    fab.addEventListener('click', (e) => {
        if (STATE.hasMoved) return;
        e.stopPropagation();
        resetIdleTimer();

        if (menu.classList.contains('visible')) {
            closeMenu();
            return;
        }

        renderMenu();
        const fabRect = fab.getBoundingClientRect();
        const isRightSide = fabRect.left > window.innerWidth / 2;
        const isBottomSide = fabRect.top > window.innerHeight / 2;

        menu.style.display = 'flex';
        // 显示遮罩层
        overlay.classList.add('active');

        const menuHeight = menu.scrollHeight || 300;
        if (isBottomSide && (fabRect.bottom + menuHeight > window.innerHeight)) {
            menu.style.top = 'auto';
            menu.style.bottom = (window.innerHeight - fabRect.bottom) + 'px';
        } else {
            menu.style.top = fabRect.top + 'px';
            menu.style.bottom = 'auto';
        }

        menu.classList.remove('opens-left', 'opens-right');
        if (isRightSide) {
            menu.style.left = (fabRect.left - CONFIG.menuWidth - CONFIG.gapSize) + 'px';
            menu.classList.add('opens-left');
        } else {
            menu.style.left = (fabRect.right + CONFIG.gapSize) + 'px';
            menu.classList.add('opens-right');
        }

        requestAnimationFrame(() => menu.classList.add('visible'));
    });

    // --- 9. 全局监听与遮罩逻辑 ---

    // 点击遮罩层关闭 (修复穿透问题)
    overlay.addEventListener('click', (e) => {
        e.stopPropagation(); // 关键:阻止事件冒泡到网页 document
        e.preventDefault();
        closeMenu();
    });

    // 窗口大小改变修正
    window.addEventListener('resize', () => {
        const rect = fab.getBoundingClientRect();
        if (rect.right > window.innerWidth) {
            fab.style.left = (window.innerWidth - 50) + 'px';
        }
        if (rect.bottom > window.innerHeight) {
            fab.style.top = (window.innerHeight - 50) + 'px';
        }
    });

    // 快捷键
    document.addEventListener('keydown', (e) => {
        const shortcut = GM_getValue('user_shortcut', '');
        if (!shortcut) return;

        const keys = shortcut.toLowerCase().split('+').map(k => k.trim());
        const pressed = {
            alt: e.altKey, ctrl: e.ctrlKey, meta: e.metaKey, shift: e.shiftKey,
            key: e.key.toLowerCase()
        };

        const mods = ['alt', 'ctrl', 'meta', 'shift'];
        const modMatch = mods.every(m => keys.includes(m) === matchMod(m));
        const mainKey = keys.find(k => !mods.includes(k));
        const keyMatch = mainKey ? (pressed.key === mainKey) : true;

        if (modMatch && keyMatch) {
            e.preventDefault();
            const cleanLink = getCleanUrl();
            handleCopy(cleanLink, '链接 (快捷键)');
        }
        function matchMod(k) { return pressed[k]; }
    });

})();