// ==UserScript== // @name IPFS CID Copy Helper // @namespace http://tampermonkey.net/ // @version 3.1 // @description 自动为网页中的 IPFS 链接和文本添加 CID 复制功能,可以管理排除网址,打开 IPFS-SCAN,以及对 CID 进行网关测速。 // @author cenglin123 // @match *://*/* // @grant GM_addStyle // @grant GM_registerMenuCommand // @grant GM_xmlhttpRequest // @grant GM_getValue // @grant GM_setValue // @homepage https://github.com/cenglin123/ipfs-cid-copy-helper // @supportURL https://github.com/cenglin123/ipfs-cid-copy-helper/issues // @license MIT // @downloadURL none // ==/UserScript== (function() { 'use strict'; //// 1. 样式及UI初始化 //// // 默认网关列表 const DEFAULT_GATEWAYS = [ 'https://ipfs.io', 'https://dweb.link', 'https://w3s.link', 'https://gw.crustgw.work', 'https://gw.crust-gateway.xyz', 'https://gateway.pinata.cloud', 'https://content.sado.space', 'https://ipfs03.food-chain.it', 'https://ipfs.hypha.coop', 'https://gw.ipfs-lens.dev', 'https://ipfs-us1.apillon.io', 'https://flk-ipfs.xyz', 'https://i0.img2ipfs.com', 'https://gw.w3ipfs.net:7443', 'https://gw.w3ipfs.cn:10443', 'https://ipfs.interface.social', 'https://ipfs-5.yoghourt.cloud ', 'https://ipfs-8.yoghourt.cloud', 'https://ipfs-11.yoghourt.cloud', 'https://ipfs-13.yoghourt.cloud', 'https://snapshot.4everland.link', 'https://lensshare.4everland.link', 'https://flair-indexing.4everland.link', 'https://ogpotheads.4everland.link', 'https://lxdaoipfs.4everland.link', 'https://ipfs.runfission.com', 'https://nftstorage.link', 'https://ipfs.raribleuserdata.com', 'https://gw.smallwolf.me', 'https://eth.sucks', ]; // 样式配置 GM_addStyle(` .ipfs-copy-btn-group { display: none; position: absolute; z-index: 10000; transform: translateX(-50%); } .ipfs-copy-btn { display: inline-block; /* 内联块级元素 */ background: #4a90e2; color: white; padding: 5px 10px; border-radius: 4px; font-size: 12px; cursor: pointer; margin: 0 2px; /* 添加水平间距 */ box-shadow: 0 2px 5px rgba(0,0,0,0.2); } .ipfs-copy-btn:hover { background: #357abd; } .ipfs-batch-buttons { position: fixed; bottom: 20px; right: 20px; display: none; /* 默认隐藏 */ flex-direction: column; gap: 10px; z-index: 10000; transition: transform 0.3s ease; height: 150px; } .ipfs-batch-buttons.visible { display: flex; /* 显示时改为 flex */ } .ipfs-batch-buttons.collapsed { transform: translateX(calc(100% + 20px)); } .ipfs-batch-btn { background: #4a90e2; color: white; padding: 8px 15px; border-radius: 4px; font-size: 14px; cursor: pointer; box-shadow: 0 2px 5px rgba(0,0,0,0.2); display: none; position: relative; white-space: nowrap; transition: transform 0.3s ease; } .ipfs-batch-btn:hover { background: #357abd; } .ipfs-copy-count { background: #5cb3ff; color: white; border-radius: 50%; padding: 2px 6px; font-size: 12px; position: absolute; top: -8px; right: -8px; } .ipfs-toggle-btn { position: absolute; left: -28px; top: 0; width: 28px; height: 28px; background: #4a90e2; color: white; border: none; border-radius: 4px 0 0 4px; cursor: pointer; display: flex; align-items: center; justify-content: center; box-shadow: -2px 0 5px rgba(0,0,0,0.2); } .ipfs-toggle-btn:hover { background: #357abd; } .ipfs-toggle-btn svg { width: 16px; height: 16px; transition: transform 0.3s ease; transform: rotate(180deg); } .collapsed .ipfs-toggle-btn svg { transform: rotate(0deg); } .ipfs-config-panel { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.2); z-index: 10001; max-width: 600px; width: 90%; max-height: 80vh; display: none; flex-direction: column; gap: 10px; } .ipfs-config-panel.visible { display: flex; } .ipfs-config-panel textarea { width: 100%; height: 200px; margin: 10px 0; padding: 8px; border: 1px solid #ddd; border-radius: 4px; resize: vertical; } .ipfs-config-panel .button-group { display: flex; gap: 10px; justify-content: flex-end; } .ipfs-config-panel button { padding: 8px 16px; border: none; border-radius: 4px; cursor: pointer; background: #4a90e2; color: white; } .ipfs-config-panel button:hover { background: #357abd; } .ipfs-config-panel h2 { margin: 0 0 10px 0; padding-bottom: 10px; border-bottom: 1px solid #ddd; } .ipfs-config-panel .help-text { font-size: 12px; color: #666; margin-bottom: 10px; } /* 网关测速窗口样式 */ .ipfs-speed-test-window { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 900px; max-width: 90vw; // height: 750px; /* 固定高度 */ max-height: 80vh; background: white; border-radius: 8px; box-shadow: 0 4px 20px rgba(0,0,0,0.3); z-index: 10001; padding: 20px; display: flex; flex-direction: column; overflow: hidden; // resize: none; /* 防止用户调整大小 */ } /* 添加标题栏拖动样式 */ .ipfs-speed-test-title { font-size: 24px; font-weight: bold; margin-top: -5px; margin-bottom: 10px; border-bottom: 1px solid #eee; padding-bottom: 10px; display: flex; justify-content: space-between; align-items: center; cursor: move; /* 显示拖动光标 */ user-select: none; /* 防止文本被选中 */ } /* 确保内容区域可滚动 */ .ipfs-speed-test-content { flex: 1; overflow-y: auto; padding-right: 10px; display: flex; flex-direction: column; gap: 15px; } .ipfs-speed-test-close { cursor: pointer; font-size: 24px; color: #666; } .ipfs-speed-test-results { max-height: 140px; overflow-y: auto; border: 1px solid #ddd; border-radius: 4px; margin-top: 5px; margin-bottom: 10px; } .ipfs-gateway-item { padding: 10px 15px; border-bottom: 1px solid #eee; display: flex; justify-content: space-between; align-items: center; cursor: pointer; transition: background-color 0.2s; } .ipfs-gateway-item:last-child { border-bottom: none; } .ipfs-gateway-item:hover { background-color: #f0f7ff; } .ipfs-gateway-item.selected { background-color: #e3f0ff; } .ipfs-gateway-url { font-weight: bold; flex: 1; } .ipfs-gateway-ping { margin-left: 10px; color: #555; width: 70px; text-align: right; } .ipfs-gateway-status { margin-left: 10px; width: 40px; text-align: center; } .ipfs-gateway-status.success { color: #4caf50; } .ipfs-gateway-status.fail { color: #f44336; } .ipfs-speed-test-progress { height: 4px; background-color: #eee; margin-top: 5px; margin-bottom: 5px; border-radius: 2px; overflow: hidden; display: none; } .ipfs-speed-test-progress-bar { height: 100%; background-color: #4a90e2; width: 0%; transition: width 0.3s; } .ipfs-speed-test-info { font-size: 14px; color: #666; margin-top: -10px; margin-bottom: 5px; display: none; height: 10px; /* 添加固定高度 */ } /* 修改链接预览区域CSS以支持横向滚动 */ .ipfs-link-preview { padding: 10px 15px; border: 1px solid #ddd; border-radius: 4px; background-color: #f5f5f5; margin-top: -5px; margin-bottom: 5px; white-space: nowrap; /* 防止文本换行 */ overflow-x: auto; /* 添加横向滚动条 */ overflow-y: hidden; /* 隐藏垂直滚动条 */ max-width: 100%; /* 限制最大宽度 */ /* 提升用户体验的额外样式 */ scrollbar-width: thin; /* Firefox中使用细滚动条 */ scrollbar-color: #ccc transparent; /* Firefox中设置滚动条颜色 */ } /* 为Webkit浏览器(Chrome、Safari等)定制滚动条样式 */ .ipfs-link-preview::-webkit-scrollbar { height: 8px; /* 滚动条高度 */ } .ipfs-link-preview::-webkit-scrollbar-track { background: transparent; /* 滚动条轨道背景 */ } .ipfs-link-preview::-webkit-scrollbar-thumb { background-color: #ccc; /* 滚动条颜色 */ border-radius: 4px; /* 滚动条圆角 */ } /* 改进支持手机触摸滚动 */ @media (pointer: coarse) { .ipfs-link-preview { -webkit-overflow-scrolling: touch; /* 在iOS上启用惯性滚动 */ padding-bottom: 15px; /* 在触摸设备上增加一些额外的空间 */ } } .ipfs-button-group { display: flex; justify-content: space-between; gap: 10px; } .ipfs-button { padding: 8px 15px; border-radius: 4px; border: none; cursor: pointer; font-size: 14px; font-weight: bold; flex: 1; display: flex; align-items: center; justify-content: center; gap: 5px; } .ipfs-primary-button { background-color: #4a90e2; color: white; } .ipfs-primary-button:hover { background-color: #357abd; } .ipfs-secondary-button { background-color: #f0f0f0; color: #333; } .ipfs-secondary-button:hover { background-color: #e0e0e0; } .ipfs-danger-button { background-color: #f5f5f5; color: #d32f2f; } .ipfs-danger-button:hover { background-color: #fbe9e7; } /* 网关管理窗口样式 */ .ipfs-gateway-manager { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 500px; max-width: 90vw; background: white; border-radius: 8px; box-shadow: 0 4px 20px rgba(0,0,0,0.3); z-index: 10002; padding: 20px; display: flex; flex-direction: column; } /* 网关管理窗口标题栏样式 */ .ipfs-gateway-manager-title { font-size: 22px; font-weight: bold; margin-top: -5px; margin-bottom: 15px; border-bottom: 1px solid #eee; padding-bottom: 10px; display: flex; justify-content: space-between; align-items: center; cursor: move; /* 显示拖动光标 */ user-select: none; /* 防止文本被选中 */ } .ipfs-gateway-manager-close { cursor: pointer; font-size: 24px; color: #666; } .ipfs-gateway-manager textarea { width: 100%; height: 150px; margin-bottom: 15px; padding: 10px; border: 1px solid #ddd; border-radius: 4px; resize: vertical; font-family: monospace; } .ipfs-gateway-manager-help { font-size: 13px; color: #666; margin-bottom: 15px; } .ipfs-selected-cid-box { padding: 10px 15px; border: 1px solid #ddd; border-radius: 4px; background-color: #f8f8f8; word-break: break-all; margin-bottom: 5px; display: flex; align-items: center; } .ipfs-selected-cid-label { font-weight: bold; margin-right: 5px; white-space: nowrap; } .ipfs-selected-cid-input { flex: 1; border: 1px solid #ddd; border-radius: 3px; padding: 5px 8px; font-family: monospace; font-size: 13px; background-color: white; } .ipfs-filename-input { flex: 1; border: 1px solid #ddd; border-radius: 3px; padding: 5px 8px; font-family: monospace; font-size: 13px; background-color: white; } `); // 创建悬停按钮 UI const copyBtnGroup = document.createElement('div'); copyBtnGroup.className = 'ipfs-copy-btn-group'; document.body.appendChild(copyBtnGroup); const copyBtn = document.createElement('div'); copyBtn.className = 'ipfs-copy-btn'; copyBtnGroup.appendChild(copyBtn); const copyLinkBtn = document.createElement('div'); copyLinkBtn.className = 'ipfs-copy-btn'; copyBtnGroup.appendChild(copyLinkBtn); //// 添加测速按钮: const speedTestBtn = document.createElement('div'); speedTestBtn.className = 'ipfs-copy-btn'; speedTestBtn.textContent = '网关测速'; copyBtnGroup.appendChild(speedTestBtn); // 创建右下角批量复制浮窗 UI const batchButtonsContainer = document.createElement('div'); batchButtonsContainer.className = 'ipfs-batch-buttons'; if (localStorage.getItem('ipfsCopyHelperDefaultCollapsed') === 'true') { // 根据默认设置决定是否添加 collapsed 类 batchButtonsContainer.classList.add('collapsed'); } document.body.appendChild(batchButtonsContainer); const toggleBtn = document.createElement('button'); toggleBtn.className = 'ipfs-toggle-btn'; toggleBtn.innerHTML = ` `; batchButtonsContainer.appendChild(toggleBtn); const batchCopyBtn = document.createElement('div'); batchCopyBtn.className = 'ipfs-batch-btn'; batchCopyBtn.innerHTML = '批量复制 CID 0'; batchButtonsContainer.appendChild(batchCopyBtn); const batchFilenameBtn = document.createElement('div'); batchFilenameBtn.className = 'ipfs-batch-btn'; batchFilenameBtn.innerHTML = '批量复制文件名 0'; batchButtonsContainer.appendChild(batchFilenameBtn); const batchDownloadBtn = document.createElement('div'); batchDownloadBtn.className = 'ipfs-batch-btn'; batchDownloadBtn.innerHTML = '批量复制下载链接 0'; batchButtonsContainer.appendChild(batchDownloadBtn); // 定义一个全局变量来存储showSpeedTestWindow函数的引用 let globalShowSpeedTestWindow = null; //// 2. 提取CID及页面扫描 //// // CID 正则表达式模式 const CID_PATTERNS = [ /\b(baf[yk][a-zA-Z0-9]{55})\b/i, // IPFS CID v1 /\b(Qm[a-zA-Z0-9]{44})\b/i, // IPFS CID v0 /\b(k51[a-zA-Z0-9]{59})\b/i // IPNS Key ]; function extractCID(input) { try { // 定义要排除的 CID const excludedCIDs = [ 'bafybeiczsscdsbs7ffqz55asqdf3smv6klcw3gofszvwlyarci47bgf354', 'QmUNLLsPACCz1vLxQVkXqqLX5R1X345qqfHbsf67hvA3Nn' ]; let extractedCID = null; // 如果输入看起来像是URL,进行URL解析 if (input.includes('://') || input.startsWith('//')) { const urlObj = new URL(input); // 匹配子域名形式 const subdomain = urlObj.hostname.split('.')[0]; if (subdomain.match(/^(baf[yk][a-zA-Z0-9]{55}|Qm[a-zA-Z0-9]{44})$/i)) { extractedCID = subdomain; } // 匹配 IPNS key if (subdomain.match(/^k51[a-zA-Z0-9]{59}$/i)) { extractedCID = subdomain; } // 匹配路径形式 const ipfsMatch = urlObj.pathname.match(/\/ipfs\/(baf[yk][a-zA-Z0-9]{55}|Qm[a-zA-Z0-9]{44})/i); if (ipfsMatch) { extractedCID = ipfsMatch[1]; } // 匹配 IPNS 路径 const ipnsMatch = urlObj.pathname.match(/\/ipns\/(k51[a-zA-Z0-9]{59})/i); if (ipnsMatch) { extractedCID = ipnsMatch[1]; } } else { // 直接尝试匹配CID格式 const cidMatch = input.match(/^(baf[yk][a-zA-Z0-9]{55}|Qm[a-zA-Z0-9]{44}|k51[a-zA-Z0-9]{59})$/i); if (cidMatch) { extractedCID = cidMatch[1]; } } // 检查是否为排除的 CID if (extractedCID && excludedCIDs.includes(extractedCID.toLowerCase())) { return null; } return extractedCID; } catch (e) { console.error('CID提取错误:', e); return null; } } // 判断是否为IPFS文件夹浏览页面 function isIPFSBrowsingPage(url) { try { const urlObj = new URL(url); // 检查子域名形式(支持 .ipfs. 和 .eth. 形式) const hostParts = urlObj.hostname.split('.'); if (hostParts.length >= 3) { const possibleCid = hostParts[0]; // 如果子域名是有效的 CID if (extractCID(possibleCid)) { // 检查是否包含 .ipfs. 或 .eth. const isIpfsDomain = hostParts.some((part, index) => index < hostParts.length - 1 && (part === 'ipfs' || part === 'eth') ); // 如果是子域名形式,且路径长度大于1(不只是根路径),则认为是浏览页面 if (isIpfsDomain && urlObj.pathname.length > 1) { return true; } } } // 检查路径形式 if (!urlObj.pathname.includes('/ipfs/')) { return false; } // 对于 Web UI 的特殊处理 if (urlObj.hash && urlObj.hash.includes('/files')) { return false; } // 普通文件浏览页面检查 const parts = urlObj.pathname.split('/'); return parts.length > 3; } catch (e) { console.error('URL解析错误:', e); return false; } } // 扫描页面中纯文本节点的函数 function scanTextNodes(node) { // 首先检查是否在排除列表中 if (isExcludedPage()) { return; } if (node.nodeType === Node.TEXT_NODE) { let hasMatch = false; let matches = []; // 收集所有匹配 for (const pattern of CID_PATTERNS) { const patternMatches = [...node.textContent.matchAll(new RegExp(pattern, 'g'))]; if (patternMatches.length > 0) { matches = matches.concat(patternMatches); hasMatch = true; } } if (hasMatch) { const container = document.createElement('span'); container.style.position = 'relative'; container.textContent = node.textContent; // 将匹配的 CID 添加到 linkInfo matches.forEach(match => { const cid = match[0]; const type = cid.startsWith('k51') ? 'IPNS Key' : 'IPFS CID'; // 只有当这个 CID 还不存在时才添加 if (!linkInfo.has(cid)) { linkInfo.set(cid, { type: type, url: null, text: cid, filename: null, isLink: false // 标记这是文本 }); } // 为每个CID创建一个内部span const cidSpan = document.createElement('span'); cidSpan.style.position = 'relative'; cidSpan.textContent = cid; cidSpan.dataset.cid = cid; cidSpan.dataset.type = type; // 替换原文本中的CID const textBefore = container.textContent.substring(0, match.index); const textAfter = container.textContent.substring(match.index + cid.length); container.textContent = ''; if (textBefore) container.appendChild(document.createTextNode(textBefore)); container.appendChild(cidSpan); if (textAfter) container.appendChild(document.createTextNode(textAfter)); }); // 添加鼠标事件监听器 container.addEventListener('mouseover', function(e) { const target = e.target; if (target.dataset && target.dataset.cid) { const rect = target.getBoundingClientRect(); showCopyButton( rect.left + (rect.width / 2), rect.bottom, target.dataset.cid, target.dataset.type ); } }); node.parentNode.replaceChild(container, node); } } else if (node.nodeType === Node.ELEMENT_NODE && !['SCRIPT', 'STYLE', 'TEXTAREA', 'A'].includes(node.tagName)) { Array.from(node.childNodes).forEach(scanTextNodes); } } // 扫描页面超链接的函数 function scanPageForLinks() { // 首先检查是否在排除列表中 if (isExcludedPage()) { console.log('当前页面在排除列表中,停止扫描'); // 清理已有的批量按钮 batchButtonsContainer.classList.remove('visible'); return; } linkInfo.clear(); // 先扫描文本节点,确保文本 CID 在后面 scanTextNodes(document.body); // 再扫描链接 const currentPageCID = extractCID(window.location.href); const currentPageBase = window.location.origin + window.location.pathname.split('/').slice(0, -1).join('/'); const links = document.getElementsByTagName('a'); for (const link of links) { const cid = extractCID(link.href); if (!cid) continue; // 只过滤无效的 CID,不再过滤当前页面的 CID try { const linkUrl = new URL(link.href); const linkBase = linkUrl.origin + linkUrl.pathname.split('/').slice(0, -1).join('/'); if (linkBase === currentPageBase) continue; const existingInfo = linkInfo.get(cid); const filename = extractFilename(link.href, link.textContent); linkInfo.set(cid, { type: detectLinkType(link.href), url: link.href, text: link.textContent.trim(), filename: filename, isLink: true }); } catch (e) { console.error('URL解析错误:', e); } } updateBatchButtons(); } // 提取文件名的函数 function extractFilename(url, linkText) { const filenameParam = new URL(url).searchParams.get('filename'); if (filenameParam) { return decodeURIComponent(filenameParam); } const pathParts = new URL(url).pathname.split('/'); const lastPart = pathParts[pathParts.length - 1]; if (lastPart && !lastPart.match(/^(Qm[a-zA-Z0-9]{44}|baf[a-zA-Z0-9]+|k51[a-zA-Z0-9]+)$/i)) { return decodeURIComponent(lastPart); } if (linkText && linkText.trim() && !linkText.includes('...')) { return linkText.trim(); } return null; } // 探测超链接IPFS类型的函数 function detectLinkType(url) { try { const urlObj = new URL(url); const subdomain = urlObj.hostname.split('.')[0]; if (subdomain.match(/^(k51[a-zA-Z0-9]{1,})$/i)) { return 'IPNS Key'; } if (subdomain.match(/^(baf[a-zA-Z0-9]{1,}|Qm[a-zA-Z0-9]{44})$/i)) { return 'IPFS CID'; } if (url.includes('/ipns/') || url.match(/k51[a-zA-Z0-9]{1,}/i)) { return 'IPNS Key'; } return 'IPFS CID'; } catch (e) { console.error('URL解析错误:', e); return 'IPFS CID'; } } // 复制某个内容到剪贴板的函数 function copyToClipboard(text, button) { const originalText = button.textContent; // 创建后备复制方案 function fallbackCopy() { try { const textArea = document.createElement('textarea'); textArea.value = text; // 将文本区域放在不可见位置 textArea.style.position = 'fixed'; textArea.style.left = '-9999px'; textArea.style.top = '-9999px'; document.body.appendChild(textArea); // 选择并复制文本 textArea.select(); const success = document.execCommand('copy'); document.body.removeChild(textArea); if (success) { button.textContent = '已复制!'; } else { button.textContent = '复制失败'; } } catch (err) { console.error('复制失败:', err); button.textContent = '复制失败'; } setTimeout(() => { button.textContent = originalText; }, 1000); } // 优先使用 Clipboard API,如果不支持则使用后备方案 if (navigator.clipboard && typeof navigator.clipboard.writeText === 'function') { navigator.clipboard.writeText(text) .then(() => { button.textContent = '已复制!'; setTimeout(() => { button.textContent = originalText; }, 1000); }) .catch(err => { console.warn('Clipboard API 失败,使用后备方案:', err); fallbackCopy(); }); } else { console.warn('Clipboard API 不可用,使用后备方案'); fallbackCopy(); } } // 批量复制函数 function batchCopyItems(type, button) { const validEntries = getValidEntries(); const totalCount = validEntries.length; if (totalCount === 0) { const originalText = button.innerHTML; button.textContent = '没有可用的项目'; setTimeout(() => { button.innerHTML = originalText; }, 1000); return; } const items = validEntries.map(([cid, info]) => { let filenameFromText = null; if (!info.isLink && info.text) { const cidIndex = info.text.indexOf(cid); if (cidIndex !== -1) { const afterCid = info.text.slice(cidIndex + cid.length); const filenameMatch = afterCid.match(/[?&]filename=([^&\n\r]+)/); if (filenameMatch) { try { filenameFromText = decodeURIComponent(filenameMatch[1]); } catch (e) { console.error('文件名解码错误:', e); } } } } switch (type) { case 'cid': return cid; case 'filename': if (info.filename && !info.filename.includes('/ipfs/')) { return info.filename; } if (filenameFromText) { return filenameFromText; } return cid; case 'url': if (info.isLink && info.url) { let url = info.url; const validFilename = info.filename && !info.filename.includes('/ipfs/') ? info.filename : (filenameFromText || cid); // 实现编码修复:先解码再编码 let decodedFilename; try { decodedFilename = decodeURIComponent(validFilename); } catch (e) { decodedFilename = validFilename; } const encodedFilename = encodeURIComponent(decodedFilename); if (!url.includes('?filename=')) { url += (url.includes('?') ? '&' : '?') + 'filename=' + encodedFilename; } else { // 如果URL已经包含filename参数,但可能有编码问题,尝试修复它 url = url.replace(/(\?|&)filename=([^&]*)/, (match, prefix, oldFilename) => `${prefix}filename=${encodedFilename}`); } return url; } const gateway = getGateway(); const filename = filenameFromText || cid; // 编码修复:先解码再编码 let decodedFilename; try { decodedFilename = decodeURIComponent(filename); } catch (e) { decodedFilename = filename; } const encodedFilename = encodeURIComponent(decodedFilename); // 修正参数名:filenames → filename return `${gateway}/ipfs/${cid}?filename=${encodedFilename}`; default: return cid; } }); const formattedItems = items.join('\n'); copyToClipboard(formattedItems, button); } function getValidEntries() { // 定义要排除的 CID 列表 const excludedCIDs = [ 'bafybeiczsscdsbs7ffqz55asqdf3smv6klcw3gofszvwlyarci47bgf354', 'QmUNLLsPACCz1vLxQVkXqqLX5R1X345qqfHbsf67hvA3Nn' ]; const currentPageCID = extractCID(window.location.href); // 获取并过滤条目 return Array.from(linkInfo.entries()) .filter(([cid]) => { // 排除当前页面的 CID if (cid === currentPageCID) return false; // 排除空文件夹 CID(根据CID版本选择比较方式) return !excludedCIDs.some(excluded => { // CID v0 (Qm开头) - 大小写敏感 if (excluded.startsWith('Qm')) { return excluded === cid; } // CID v1 (baf开头) - 大小写不敏感 return excluded.toLowerCase() === cid.toLowerCase(); }); }); } // 更新显示计数的函数 function updateBatchButtons() { const validEntries = getValidEntries(); const totalCount = validEntries.length; if (totalCount > 0) { batchButtonsContainer.classList.add('visible'); // 更新批量复制CID按钮 batchCopyBtn.style.display = 'block'; batchCopyBtn.innerHTML = `批量复制CID ${totalCount}`; // 更新批量复制文件名按钮 batchFilenameBtn.style.display = 'block'; batchFilenameBtn.innerHTML = `批量复制文件名 ${totalCount}`; // 更新批量复制下载链接按钮 batchDownloadBtn.style.display = 'block'; batchDownloadBtn.innerHTML = `批量复制下载链接 ${totalCount}`; } else { batchButtonsContainer.classList.remove('visible'); [batchCopyBtn, batchDownloadBtn, batchFilenameBtn].forEach(btn => { btn.style.display = 'none'; }); } } // 调用统一的复制处理函数 function batchCopyCIDs() { batchCopyItems('cid', batchCopyBtn); } function batchCopyFilenames() { batchCopyItems('filename', batchFilenameBtn); } function batchCopyDownloadLinks() { batchCopyItems('url', batchDownloadBtn); } // 显示复制按钮 function showCopyButton(x, y, cid, type, isLink = false, url = null) { // 如果在排除列表中,不显示按钮 if (isExcludedPage()) { return; } // 清除可能存在的定时器 if (hideTimeout) { clearTimeout(hideTimeout); hideTimeout = null; } if (showTimeout) { clearTimeout(showTimeout); showTimeout = null; } // 保存CID和相关信息到按钮组,供网关测速按钮使用 copyBtnGroup.dataset.lastCid = cid; copyBtnGroup.dataset.lastType = type; // 保存URL信息,如果可用 if (url) { copyBtnGroup.dataset.lastUrl = url; } else { delete copyBtnGroup.dataset.lastUrl; } copyBtnGroup.style.display = 'block'; copyBtnGroup.style.top = `${y + window.scrollY + 5}px`; copyBtnGroup.style.left = `${x + window.scrollX}px`; copyBtn.textContent = `复制 ${type}`; copyBtn.onclick = () => { copyToClipboard(cid, copyBtn); }; // 只为纯文本CID显示下载链接按钮 if (!isLink) { copyLinkBtn.style.display = 'inline-block'; copyLinkBtn.textContent = '复制下载链接'; copyLinkBtn.onclick = () => { const gateway = getGateway(); const downloadLink = `${gateway}/ipfs/${cid}`; copyToClipboard(downloadLink, copyLinkBtn); }; } else { copyLinkBtn.style.display = 'none'; } // 显示网关测速按钮 speedTestBtn.style.display = 'inline-block'; speedTestBtn.textContent = '网关测速'; } // 修改隐藏按钮函数 function hideButton() { // 如果在排除列表中,确保按钮隐藏 if (isExcludedPage()) { copyBtnGroup.style.display = 'none'; return; } if (hideTimeout) { clearTimeout(hideTimeout); } if (showTimeout) { clearTimeout(showTimeout); } hideTimeout = setTimeout(() => { if (!isButtonHovered && !currentHoveredElement) { copyBtnGroup.style.display = 'none'; } }, 150); } let scanTimeout; function initPageScan() { if (isExcludedPage()) { console.log('当前页面在排除列表中,不初始化扫描'); // 确保批量按钮被隐藏 batchButtonsContainer.classList.remove('visible'); return; } if (scanTimeout) { clearTimeout(scanTimeout); } scanTimeout = setTimeout(scanPageForLinks, 1000); } //// 3. 鼠标悬停事件处理 //// // 初始化状态变量 let currentHoveredElement = null; let currentHoveredLink = null; // 添加这个变量声明 let isButtonHovered = false; let hideTimeout = null; let showTimeout = null; // 统一处理文本和链接的悬停 function handleElementHover(element, cid, type, isLink = false, url = null) { // 首先检查是否在排除列表中 if (isExcludedPage()) { return; } if (currentHoveredElement === element) return; // 如果是同一个元素,不重复处理 currentHoveredElement = element; const rect = element.getBoundingClientRect(); // 使用延时显示,避免快速划过时的闪烁 if (showTimeout) { clearTimeout(showTimeout); } showTimeout = setTimeout(() => { showCopyButton( rect.left + (rect.width / 2), rect.bottom, cid, type, isLink, url ); }, 50); } // mouseover 事件处理 document.addEventListener('mouseover', function(e) { // 如果在排除列表中,直接返回 if (isExcludedPage()) { return; } // 处理链接 const link = e.target.closest('a'); if (link) { const href = link.href; if (!href) return; const linkCID = extractCID(href); if (!linkCID) return; const shouldShow = isIPFSBrowsingPage(window.location.href) || linkCID !== extractCID(window.location.href); if (shouldShow) { handleElementHover(link, linkCID, detectLinkType(href), true, href); return; } } // 处理文本节点中的 CID const cidSpan = e.target.closest('[data-cid]'); if (cidSpan && cidSpan.dataset.cid) { handleElementHover(cidSpan, cidSpan.dataset.cid, cidSpan.dataset.type, false); return; } }); // mousemove 事件处理 document.addEventListener('mousemove', function(e) { const overLink = e.target.closest('a'); const overButton = e.target.closest('.ipfs-copy-btn-group'); if (!overLink && !overButton) { currentHoveredLink = null; isButtonHovered = false; hideButton(); } }); // mouseout 事件处理 document.addEventListener('mouseout', function(e) { const relatedTarget = e.relatedTarget; if (!relatedTarget || (!relatedTarget.closest('.ipfs-copy-btn-group') && !relatedTarget.dataset?.cid && !relatedTarget.closest('a'))) { currentHoveredElement = null; currentHoveredLink = null; hideButton(); } }); // 按钮组的事件监听 copyBtnGroup.addEventListener('mouseover', function() { if (isExcludedPage()) { return; } isButtonHovered = true; if (hideTimeout) { clearTimeout(hideTimeout); } if (showTimeout) { clearTimeout(showTimeout); } }); copyBtnGroup.addEventListener('mouseout', function(e) { if (isExcludedPage()) { return; } isButtonHovered = false; const relatedTarget = e.relatedTarget; if (!relatedTarget || (!relatedTarget.dataset?.cid && !relatedTarget.closest('a') && !relatedTarget.closest('.ipfs-copy-btn-group'))) { hideButton(); } }); // observer 的设置,观察DOM变化 let scanScheduled = false; const observer = new MutationObserver(() => { if (!scanScheduled) { scanScheduled = true; setTimeout(() => { initPageScan(); scanScheduled = false; }, 1500); // 在1500ms后再执行扫描,避免频繁触发 } }); observer.observe(document.body, { childList: true, subtree: true }); //// 4. 网关测速逻辑实现 //// // 创建测速结果窗口 const speedTestWindow = document.createElement('div'); speedTestWindow.className = 'ipfs-speed-test-window'; speedTestWindow.style.display = 'none'; document.body.appendChild(speedTestWindow); // 创建网关管理窗口 const gatewayManagerWindow = document.createElement('div'); gatewayManagerWindow.className = 'ipfs-gateway-manager'; gatewayManagerWindow.style.display = 'none'; document.body.appendChild(gatewayManagerWindow); // 填充测速窗口内容 speedTestWindow.innerHTML = `