您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
在磁力链接后添加标识符号,通过点击或悬停显示完整链接信息,如果选中的文本中包含磁力链接或磁力链接特征码,在附近添加悬浮标志,悬停预览链接内容
// ==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); })();