Whatslink磁力预览

在磁力链接后添加标识符号,通过点击或悬停显示完整链接信息,如果选中的文本中包含磁力链接或磁力链接特征码,在附近添加悬浮标志,悬停预览链接内容

// ==UserScript==
// @name         Whatslink磁力预览
// @namespace    http://whatslink.info/
// @version      2.8.1
// @description  在磁力链接后添加标识符号,通过点击或悬停显示完整链接信息,如果选中的文本中包含磁力链接或磁力链接特征码,在附近添加悬浮标志,悬停预览链接内容
// @author       sexjpg
// @grant        GM_xmlhttpRequest
// @grant        GM_notification
// @grant        GM_setValue
// @grant        GM_getValue
// @connect      whatslink.info
// @match        *://*6v520.com*/*
// @match        *://*javdb*.*/*
// @match        *://*javbus*.*/*
// @match        *://*.*/*


// @noframes
// @run-at       document-end

// @license MIT

// ==/UserScript==

(function () {
    'use strict';

    // 配置参数
    const CONFIG = {
        delay: 500, // 悬浮延迟时间(毫秒)
        cacheTTL: 1*24*60 * 60 * 1000, // 缓存有效期(天)
        indicator_innerhtml: '🧲'
    };

    // 缓存对象,使用{}
    const magnetCache = GM_getValue('magnetCache', {});

    // 创建悬浮框容器
    const tooltip = document.createElement('div');
    tooltip.style.cssText = `
    position: fixed;
    max-width: 400px;
    min-width: 300px;
    padding: 15px;
    background: rgba(0, 0, 0, 0.95);
    color: #fff;
    border-radius: 8px;
    font-size: 14px;
    font-family: Arial, sans-serif;
    z-index: 9999;
    pointer-events: auto; /* 修改为 auto,允许鼠标事件 */
    word-break: break-all;
    opacity: 0;
    transition: opacity 0.3s ease, transform 0.3s ease;
    transform: scale(0.95);
    box-shadow: 0 4px 12px rgba(0,0,0,0.4);
    display: none;
`;

    // 新增:创建图片放大预览容器
    const imageModal = document.createElement('div');
    imageModal.style.cssText = `
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    background: rgba(0, 0, 0, 0.9);
    display: none;
    justify-content: center;
    align-items: center;
    z-index: 10000;
    cursor: zoom-out;
`;

    const modalImage = document.createElement('img');
    modalImage.style.cssText = `
    max-width: 90%;
    max-height: 90%;
    object-fit: contain;
    cursor: auto;
`;

    // 添加左右切换按钮
    const prevButton = document.createElement('div');
    // prevButton.innerHTML = '❮';
    prevButton.style.cssText = `
        position: absolute;
        left: 0;
        top: 0;
        bottom: 0;
        width: 25vw;
        display: flex;
        align-items: center;
        justify-content: center;
        font-size: 40px;
        color: white;
        cursor: pointer;
        user-select: none;
        z-index: 10001;
        opacity: 0.3;
        transition: opacity 0.3s ease;
    `;


    const nextButton = document.createElement('div');
    // nextButton.innerHTML = '❯';
    nextButton.style.cssText = `
        position: absolute;
        right: 0;
        top: 0;
        bottom: 0;
        width: 25vw;
        display: flex;
        align-items: center;
        justify-content: center;
        font-size: 40px;
        color: white;
        cursor: pointer;
        user-select: none;
        z-index: 10001;
        opacity: 0.3;
        transition: opacity 0.3s ease;
    `;
        // 鼠标悬停时增加透明度
    prevButton.addEventListener('mouseenter', () => {
        prevButton.style.opacity = '0.7';
    });
    prevButton.addEventListener('mouseleave', () => {
        prevButton.style.opacity = '0.3';
    });
    nextButton.addEventListener('mouseenter', () => {
        nextButton.style.opacity = '0.7';
    });
    nextButton.addEventListener('mouseleave', () => {
        nextButton.style.opacity = '0.3';
    });

    imageModal.appendChild(prevButton);
    imageModal.appendChild(nextButton);
    imageModal.appendChild(modalImage);
    document.body.appendChild(imageModal);

    // 点击模态框关闭
    imageModal.addEventListener('click', (e) => {
        // 只有点击模态框背景时才关闭,点击按钮或图片时不关闭
        if (e.target === imageModal) {
            imageModal.style.display = 'none';
        }
    });

    // 新增变量用于控制 tooltip 状态
    let tooltipHideTimer = null;
    let isTooltipHovered = false;

    // 存储当前tooltip中的截图信息
    let currentScreenshots = [];
    let currentScreenshotIndex = 0;

    document.body.appendChild(tooltip);

    // 磁力链接检测正则
    const magnetRegex = /^magnet:\?xt=urn:btih:([a-fA-F0-9]{40})(?:&|$)/i;

    // 标识符号样式
    const indicatorStyle = `
        display: inline-block;
        width: 16px;
        height: 16px;
        background: #007bff;
        border-radius: 50%;
        color: white;
        text-align: center;
        font-size: 12px;
        margin-left: 4px;
        cursor: progress;
        user-select: none;
        vertical-align: middle;
        transition: all 0.2s ease;
    `;

    // 获取磁力链接特征码
    function getMagnetHash(magnetLink) {
        const match = magnetLink.match(magnetRegex);
        return match ? match[1].toLowerCase() : null;
    }

    // API请求函数(修正GET请求方式)
    function fetchMagnetInfo(magnetLink, callback) {
        try {
            GM_xmlhttpRequest({
                method: 'GET',
                url: `https://whatslink.info/api/v1/link?url=${magnetLink}`,
                headers: { 'Content-Type': "text/plain", },
                onload: function (response) {
                    try {
                        const data = JSON.parse(response.responseText);
                        console.debug('网络请求数据', data);
                        // 只缓存有效数据,有数据,且数据无错误,且文件类型不为空
                        if (data && !data.error && data.file_type) {
                            const hash = getMagnetHash(magnetLink);
                            if (hash) {
                                magnetCache[hash] = {
                                    data: data,
                                    expiresAt: Date.now() + CONFIG.cacheTTL
                                };
                                // 保存缓存
                                console.debug('更新缓存', magnetCache[hash]);
                                GM_setValue('magnetCache', magnetCache);
                                console.debug('更新缓存完成,总缓存数量:', Object.keys(magnetCache).length);
                            }
                        }

                        callback(null, data);
                    } catch (error) {
                        callback(new Error('解析响应数据失败: ' + error.message));
                    }
                },
                onerror: function (error) {
                    callback(new Error('API请求失败: ' + error.statusText));
                }
            });
        } catch (error) {
            callback(new Error('请求异常: ' + error.message));
        }
    }

    // 检查缓存
    function checkCache(magnetLink) {
        const hash = getMagnetHash(magnetLink);
        console.debug('开始检索缓存,缓存总量', Object.keys(magnetCache).length, magnetCache);
        console.debug('检索特征码', hash);
        if (!hash || !magnetCache[hash]) {
            console.debug('缓存中未检索到特征码:', hash);
            return null
        };

        // 检查缓存是否过期
        if (Date.now() > magnetCache[hash].expiresAt) {
            delete magnetCache[hash];
            console.debug('缓存特征码过期', hash);
            return null;
        }
        console.debug('获取缓存数据', magnetCache[hash]);
        return magnetCache[hash].data;
    }

    // 数据展示函数
    function renderMagnetInfo(data) {
        let html = `
            <div style="margin-bottom: 10px;">
                <strong style="font-size: 16px; word-break: break-word;">${data.name || '未知名称'}</strong>
            </div>
            
            <div style="margin-bottom: 8px;">
                <span>类型:</span>
                <span style="color: #17a2b8;">${data.type || '未知类型'}</span>
            </div>
            
            <div style="margin-bottom: 8px;">
                <span>文件类型:</span>
                <span style="color: #ffc107;">${data.file_type || '未知文件类型'}</span>
            </div>
            
            <div style="margin-bottom: 8px;">
                <span>大小:</span>
                <span style="color: #28a745;">${formatFileSize(data.size) || '未知大小'}</span>
            </div>
            
            <div style="margin-bottom: 8px;">
                <span>文件数:</span>
                <span style="color: #dc3545;">${data.count || 0}</span>
            </div>
        `;

        if (data.screenshots && data.screenshots.length > 0) {
            html += `<div style="margin-top: 15px; display: flex; flex-wrap: wrap; gap: 5px;">`;
            data.screenshots.slice(0, 5).forEach(screenshot => {
                html += `
                    <div style="flex: 1 1 45%; min-width: 100px; cursor: zoom-in;" class="screenshot-item" data-src="${screenshot.screenshot}">
                        <img src="${screenshot.screenshot}" 
                             style="width: 100%; border-radius: 4px; box-shadow: 0 2px 6px rgba(0,0,0,0.3);">
                    </div>
                `;
            });
            html += `</div>`;
        }

        return html;
    }

    // 格式化文件大小
    function formatFileSize(bytes) {
        if (bytes === undefined || bytes === null) return '未知大小';
        if (bytes === 0) return '0 Bytes';

        const k = 1024;
        const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
        const i = Math.floor(Math.log(bytes) / Math.log(k));

        return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
    }

    // 显示悬浮框的核心逻辑
    function showTooltip(magnetLink, event) {
        // 检查缓存
        const cachedData = checkCache(magnetLink);
        if (cachedData) {
            // 使用缓存数据
            tooltip.innerHTML = renderMagnetInfo(cachedData);
            updateTooltipPosition(event);
            tooltip.style.display = 'block';
            tooltip.style.opacity = '1';
            tooltip.style.transform = 'scale(1)';
            
            // 新增:为截图添加点击放大事件
            addScreenshotClickEvents(cachedData.screenshots || []);
            return;
        }

        // 显示加载状态
        tooltip.innerHTML = '<div style="text-align: center; padding: 10px;">加载中...</div>';
        tooltip.style.display = 'block';
        tooltip.style.opacity = '1';
        tooltip.style.transform = 'scale(1)';
        updateTooltipPosition(event);

        // 请求API数据
        fetchMagnetInfo(magnetLink, (error, data) => {
            if (error) {
                tooltip.innerHTML = `<div style="color: #dc3545; text-align: center; padding: 10px;">${error.message}</div>`;
            } else {
                tooltip.innerHTML = renderMagnetInfo(data);
                
                // 新增:为截图添加点击放大事件
                addScreenshotClickEvents(data.screenshots || []);
            }
            updateTooltipPosition(event);
        });

    }

    // 新增:为截图添加点击放大事件的函数
    function addScreenshotClickEvents(screenshots) {
        const screenshotItems = tooltip.querySelectorAll('.screenshot-item');
        screenshotItems.forEach((item, index) => {
            item.addEventListener('click', (e) => {
                e.stopPropagation();
                const src = item.getAttribute('data-src');
                modalImage.src = src;
                imageModal.style.display = 'flex';
                
                // 保存当前截图信息
                currentScreenshots = screenshots;
                currentScreenshotIndex = index;
                
                // 根据是否有前后图片决定是否显示按钮
                updateNavigationButtons();
            });
        });
    }
    
    // 更新导航按钮状态
    function updateNavigationButtons() {
        prevButton.style.display = 'block'
        nextButton.style.display = 'block'
        // prevButton.style.display = currentScreenshotIndex > 0 ? 'block' : 'none';
        // nextButton.style.display = currentScreenshotIndex < currentScreenshots.length - 1 ? 'block' : 'none';
    }


// 切换到上一张图片
function showPrevImage() {
    if (currentScreenshotIndex > 0) {
        currentScreenshotIndex--;
    } else {
        // 新增:循环到最后一张
        currentScreenshotIndex = currentScreenshots.length - 1;
    }
    modalImage.src = currentScreenshots[currentScreenshotIndex].screenshot;
    updateNavigationButtons();
}

// 切换到下一张图片
function showNextImage() {
    if (currentScreenshotIndex < currentScreenshots.length - 1) {
        currentScreenshotIndex++;
    } else {
        // 新增:循环到第一张
        currentScreenshotIndex = 0;
    }
    modalImage.src = currentScreenshots[currentScreenshotIndex].screenshot;
    updateNavigationButtons();
}


    // 绑定左右按钮点击事件
    prevButton.addEventListener('click', (e) => {
        e.stopPropagation();
        showPrevImage();
    });

    nextButton.addEventListener('click', (e) => {
        e.stopPropagation();
        showNextImage();
    });

    // 支持键盘左右键切换
    imageModal.addEventListener('keydown', (e) => {
        if (imageModal.style.display !== 'none') {
            if (e.key === 'ArrowLeft') {
                showPrevImage();
            } else if (e.key === 'ArrowRight') {
                showNextImage();
            }
        }
    });

    // 更新悬浮框位置
    function updateTooltipPosition(e) {
        const tooltipRect = tooltip.getBoundingClientRect();
        const viewportWidth = window.innerWidth;
        let x = e.clientX + 15;
        //本身是y = e.clientY + 15,改为往上调整15个像素
        let y = e.clientY - 15;

        // 防止超出右侧视口
        if (x + tooltipRect.width > viewportWidth - 20) {
            x = e.clientX - tooltipRect.width - 15;
        }

        tooltip.style.left = `${x}px`;
        tooltip.style.top = `${y}px`;
    }

    // 处理单个链接元素
    function processLink(link) {
        // 检查是否是磁力链接且未被处理过
        if (link.dataset.magnetProcessed || !magnetRegex.test(link.href)) {
            return;
        }
        link.dataset.magnetProcessed = 'true'; // 标记为已处理

        let timer = null;
        let isHovered = false; // 新增悬停状态

        const indicator = document.createElement('span');
        indicator.innerHTML = CONFIG.indicator_innerhtml;
        indicator.style.cssText = indicatorStyle;
        link.appendChild(indicator);

        // 鼠标进入事件
        indicator.addEventListener('mouseenter', (e) => {
            clearTimeout(tooltipHideTimer); // 清除之前的隐藏计时器
            timer = setTimeout(() => {
                showTooltip(link.href, e);
            }, CONFIG.delay);
        });

        indicator.addEventListener('mouseleave', () => {
            clearTimeout(timer); // 取消未触发的显示
            // 不再立即隐藏 tooltip,交给 tooltip 自己控制
        });

        indicator.addEventListener('click', (e) => {
            e.preventDefault();
            e.stopPropagation();
            clearTimeout(timer);
            showTooltip(link.href, e);
        });

        tooltip.addEventListener('mouseenter', () => {
            isTooltipHovered = true;
            clearTimeout(tooltipHideTimer);
        });

        tooltip.addEventListener('mouseleave', () => {
            isTooltipHovered = false;
            tooltipHideTimer = setTimeout(() => {
                tooltip.style.opacity = '0';
                tooltip.style.transform = 'scale(0.95)';
                setTimeout(() => {
                    tooltip.style.display = 'none';
                }, 300); // 与 transition 时间匹配
            }, CONFIG.delay);
        });
    }

    // 新增:处理选中文本中的磁力链接
    function processSelectedText() {
        const selection = window.getSelection();
        if (selection.rangeCount === 0) return;
        
        const range = selection.getRangeAt(0);
        // const selectedText = range.toString().trim();
        
        // 修改这行代码,移除所有空白字符(包括空格、换行符、制表符等)
        const selectedText = range.toString().replace(/\s/g, '');

        console.debug('选中文字:', selectedText);
        
        // 新增:检查是否为40位十六进制字符串
        const hexHashRegex = /^[a-fA-F0-9]{40}$/;
        let processedText = selectedText;
        let isMagnetLink = magnetRegex.test(selectedText);
        
        // 如果是40位十六进制字符串,构造完整磁力链接
        if (hexHashRegex.test(selectedText)) {
            processedText = `magnet:?xt=urn:btih:${selectedText}`;
            isMagnetLink = true;
        }
        
        // 检查选中的文本是否是磁力链接
        if (isMagnetLink) {
            // 检查是否已经添加过指示器
            const existingIndicator = document.getElementById('magnet-selection-indicator');
            if (existingIndicator) {
                existingIndicator.remove();
            }
            
            // 创建指示器
            const indicator = document.createElement('span');
            indicator.id = 'magnet-selection-indicator';
            indicator.innerHTML = CONFIG.indicator_innerhtml;
            indicator.style.cssText = indicatorStyle;
            
            // 在选中文本末尾插入指示器
            const rect = range.getBoundingClientRect();
            indicator.style.position = 'fixed';
            indicator.style.left = `${rect.right + 5}px`;
            // indicator.style.top = `${rect.top + window.scrollY}px`;
            indicator.style.top = `${rect.top}px`;
            indicator.style.zIndex = '99999';
            
            document.body.appendChild(indicator);
            
            let timer = null;
            let isTooltipShownByThisIndicator = false; // 标记这个指示器是否显示了tooltip
            
            // 添加事件监听
            indicator.addEventListener('mouseenter', (e) => {
                clearTimeout(tooltipHideTimer);
                timer = setTimeout(() => {
                    showTooltip(processedText, e);
                    isTooltipShownByThisIndicator = true;
                }, CONFIG.delay);
            });
            
            indicator.addEventListener('mouseleave', () => {
                clearTimeout(timer);
                // 如果这个指示器显示了tooltip,则添加mouseleave处理逻辑
                if (isTooltipShownByThisIndicator) {
                    tooltipHideTimer = setTimeout(() => {
                        tooltip.style.opacity = '0';
                        tooltip.style.transform = 'scale(0.95)';
                        setTimeout(() => {
                            tooltip.style.display = 'none';
                        }, 300);
                    }, CONFIG.delay);
                }
            });
            
            indicator.addEventListener('click', (e) => {
                e.preventDefault();
                e.stopPropagation();
                clearTimeout(timer);
                showTooltip(processedText, e);
                isTooltipShownByThisIndicator = true;
            });
            
            // 当选择改变时移除指示器
            document.addEventListener('selectionchange', function removeIndicator() {
                if (document.getElementById('magnet-selection-indicator')) {
                    document.getElementById('magnet-selection-indicator').remove();
                }
                document.removeEventListener('selectionchange', removeIndicator);
            }, { once: true });


        tooltip.addEventListener('mouseenter', () => {
            isTooltipHovered = true;
            clearTimeout(tooltipHideTimer);
        });

        tooltip.addEventListener('mouseleave', () => {
            isTooltipHovered = false;
            tooltipHideTimer = setTimeout(() => {
                tooltip.style.opacity = '0';
                tooltip.style.transform = 'scale(0.95)';
                setTimeout(() => {
                    tooltip.style.display = 'none';
                }, 300); // 与 transition 时间匹配
            }, CONFIG.delay);
        });
        }
    }

    // 使用 MutationObserver 监听动态内容
    function observeDOMChanges() {
        const observer = new MutationObserver(mutations => {
            mutations.forEach(mutation => {
                mutation.addedNodes.forEach(node => {
                    // 只处理元素节点
                    if (node.nodeType === 1) {
                        // 检查节点本身是否是链接
                        if (node.tagName === 'A') {
                            processLink(node);
                        }
                        // 检查节点下的所有链接
                        node.querySelectorAll('a').forEach(processLink);
                    }
                });
            });
        });

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

    // 初始执行 + 启动监听
    document.querySelectorAll('a').forEach(processLink); // 处理页面已有的链接
    observeDOMChanges(); // 监听后续动态添加的链接
    
    // 监听鼠标抬起事件,用于检测选中文本
    document.addEventListener('mouseup', processSelectedText);
})();