// ==UserScript== // @name IPFS CID Copy Helper // @namespace http://tampermonkey.net/ // @version 3.5 // @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 https://update.greasyfork.icu/scripts/514613/IPFS%20CID%20Copy%20Helper.user.js // @updateURL https://update.greasyfork.icu/scripts/514613/IPFS%20CID%20Copy%20Helper.meta.js // ==/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://ipfs.hypha.coop', 'https://gw.ipfs-lens.dev', 'https://i0.img2ipfs.com', '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://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: 15px; left: 50%; transform: translateX(-50%); width: min(450px, 95vw); max-height: calc(100vh - 30px); min-height: 300px; background: white; border-radius: 8px; box-shadow: 0 4px 20px rgba(0,0,0,0.3); z-index: 10001; padding: 0; display: flex; flex-direction: column; overflow: hidden; border: 1px solid #e0e0e0; } /* 标题栏优化 */ .ipfs-speed-test-title { font-size: 16px; font-weight: 600; margin: 0; padding: 12px 15px 10px 15px; border-bottom: 1px solid #eee; display: flex; justify-content: space-between; align-items: center; cursor: move; user-select: none; background: #f8f9fa; border-radius: 8px 8px 0 0; min-height: 18px; flex-shrink: 0; } /* 内容区域 - 紧凑间距 */ .ipfs-speed-test-content { flex: 1; overflow-y: auto; padding: 12px 15px 15px 15px; display: flex; flex-direction: column; gap: 8px; min-height: 0; } /* 关闭按钮 */ .ipfs-speed-test-close { cursor: pointer; font-size: 18px; color: #666; padding: 3px; border-radius: 3px; transition: all 0.2s ease; line-height: 1; width: 24px; height: 24px; display: flex; align-items: center; justify-content: center; } .ipfs-speed-test-close:hover { background-color: #f0f0f0; color: #333; } /* 测试结果区域 */ .ipfs-speed-test-results { max-height: 160px; overflow-y: auto; border: 1px solid #ddd; border-radius: 6px; margin: 0; background: #fafafa; } .ipfs-gateway-item { padding: 8px 12px; border-bottom: 1px solid #eee; display: flex; justify-content: space-between; align-items: center; cursor: pointer; transition: all 0.2s ease; background: white; } .ipfs-gateway-item:last-child { border-bottom: none; } .ipfs-gateway-item:hover { background-color: #f0f7ff; } .ipfs-gateway-item.selected { background-color: #e3f0ff; border-left: 4px solid #4a90e2; } .ipfs-gateway-url { font-weight: 500; flex: 1; font-family: 'Consolas', 'Monaco', monospace; font-size: 12px; margin-right: 8px; } .ipfs-gateway-ping { color: #555; width: 50px; text-align: right; font-weight: 500; font-size: 12px; } .ipfs-gateway-status { margin-left: 6px; width: 30px; text-align: center; font-weight: bold; font-size: 12px; } .ipfs-gateway-status.success { color: #4caf50; } .ipfs-gateway-status.fail { color: #f44336; } /* 进度条 */ .ipfs-speed-test-progress { height: 4px; background-color: #eee; margin: 4px 0; border-radius: 2px; overflow: hidden; display: none; } .ipfs-speed-test-progress-bar { height: 100%; background: linear-gradient(90deg, #4a90e2, #5cb3ff); width: 0%; transition: width 0.3s ease; border-radius: 2px; } /* 信息提示 */ .ipfs-speed-test-info { font-size: 13px; color: #666; margin: 0; display: none; height: 14px; line-height: 14px; } /* 链接预览 */ .ipfs-link-preview { padding: 8px 12px; border: 1px solid #ddd; border-radius: 6px; background-color: #f8f9fa; margin: 0; white-space: nowrap; overflow-x: auto; overflow-y: hidden; max-width: 100%; font-family: 'Consolas', 'Monaco', monospace; font-size: 12px; scrollbar-width: thin; scrollbar-color: #ccc transparent; } .ipfs-link-preview::-webkit-scrollbar { height: 6px; } .ipfs-link-preview::-webkit-scrollbar-track { background: transparent; } .ipfs-link-preview::-webkit-scrollbar-thumb { background-color: #ccc; border-radius: 3px; } /* 移动端优化 */ @media (pointer: coarse) { .ipfs-link-preview { -webkit-overflow-scrolling: touch; padding-bottom: 15px; } .ipfs-speed-test-window { width: 95vw; max-height: calc(100vh - 20px); top: 10px; } .ipfs-speed-test-title { padding: 20px 15px 15px 15px; min-height: 25px; } } /* 按钮组样式保持不变 */ .ipfs-button-group { display: flex; justify-content: space-between; gap: 10px; margin-top: 10px; } /* 按钮网格布局 - 移动端优化 */ .ipfs-button-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 6px; margin-top: 5px; } .ipfs-button { padding: 8px 10px; border-radius: 5px; border: none; cursor: pointer; font-size: 13px; font-weight: 500; display: flex; align-items: center; justify-content: center; gap: 4px; transition: all 0.2s ease; min-height: 36px; } .ipfs-button.ipfs-full-width { grid-column: 1 / -1; } .ipfs-btn-text { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .ipfs-primary-button { background-color: #4a90e2; color: white; } .ipfs-primary-button:hover { background-color: #357abd; transform: translateY(-1px); } .ipfs-secondary-button { background-color: #f0f0f0; color: #333; } .ipfs-secondary-button:hover { background-color: #e0e0e0; transform: translateY(-1px); } .ipfs-danger-button { background-color: #f5f5f5; color: #d32f2f; } .ipfs-danger-button:hover { background-color: #fbe9e7; transform: translateY(-1px); } /* 网关管理窗口 */ .ipfs-gateway-manager { position: fixed; top: 30px; left: 50%; transform: translateX(-50%); width: min(400px, 90vw); max-height: calc(100vh - 60px); background: white; border-radius: 8px; box-shadow: 0 4px 20px rgba(0,0,0,0.3); z-index: 10002; padding: 0; display: flex; flex-direction: column; border: 1px solid #e0e0e0; } .ipfs-gateway-manager-title { font-size: 16px; font-weight: 600; margin: 0; padding: 12px 15px 10px 15px; border-bottom: 1px solid #eee; display: flex; justify-content: space-between; align-items: center; cursor: move; user-select: none; background: #f8f9fa; border-radius: 8px 8px 0 0; flex-shrink: 0; } .ipfs-gateway-manager-close { cursor: pointer; font-size: 18px; color: #666; padding: 3px; border-radius: 3px; transition: all 0.2s ease; width: 24px; height: 24px; display: flex; align-items: center; justify-content: center; } .ipfs-gateway-manager-close:hover { background-color: #f0f0f0; color: #333; } .ipfs-gateway-manager-content { padding: 12px 15px 15px 15px; flex: 1; overflow-y: auto; } .ipfs-gateway-manager textarea { width: 100%; height: 120px; margin-bottom: 10px; padding: 8px 10px; border: 1px solid #ddd; border-radius: 6px; resize: vertical; font-family: 'Consolas', 'Monaco', monospace; font-size: 12px; } .ipfs-gateway-manager-help { font-size: 12px; color: #666; margin-bottom: 10px; line-height: 1.3; } /* 移动端按钮组 */ .ipfs-button-group.ipfs-mobile-buttons { display: flex; gap: 8px; margin-top: 8px; } .ipfs-button-group .ipfs-button { flex: 1; } /* 移动端特定优化 */ @media screen and (max-width: 480px) { .ipfs-speed-test-window { width: 95vw; top: 10px; max-height: calc(100vh - 20px); } .ipfs-speed-test-title, .ipfs-gateway-manager-title { font-size: 15px; padding: 10px 12px 8px 12px; } .ipfs-speed-test-content, .ipfs-gateway-manager-content { padding: 10px 12px 12px 12px; gap: 6px; } .ipfs-selected-cid-box { padding: 6px 10px; flex-direction: column; align-items: stretch; gap: 4px; } .ipfs-selected-cid-label { font-size: 12px; } .ipfs-selected-cid-input, .ipfs-filename-input { font-size: 11px; padding: 4px 6px; } .ipfs-button-grid { grid-template-columns: 1fr; gap: 4px; } .ipfs-button { padding: 6px 8px; font-size: 12px; min-height: 32px; } .ipfs-gateway-url { font-size: 11px; } .ipfs-gateway-ping, .ipfs-gateway-status { font-size: 11px; } .ipfs-gateway-manager { width: 95vw; top: 10px; } .ipfs-gateway-manager textarea { height: 100px; font-size: 11px; } } /* 超小屏幕优化 */ @media screen and (max-height: 500px) { .ipfs-speed-test-window, .ipfs-gateway-manager { top: 5px; max-height: calc(100vh - 10px); } .ipfs-speed-test-results { max-height: 100px; } .ipfs-gateway-manager textarea { height: 80px; } } /* CID输入框优化 */ .ipfs-selected-cid-box { padding: 8px 12px; border: 1px solid #ddd; border-radius: 6px; background-color: #f8f9fa; display: flex; align-items: center; gap: 6px; margin-bottom: 0; } .ipfs-selected-cid-label { font-weight: 500; white-space: nowrap; color: #333; font-size: 13px; min-width: fit-content; } .ipfs-selected-cid-input { flex: 1; border: 1px solid #ddd; border-radius: 4px; padding: 6px 10px; font-family: 'Consolas', 'Monaco', monospace; font-size: 13px; background-color: white; transition: border-color 0.2s ease; } .ipfs-selected-cid-input:focus { outline: none; border-color: #4a90e2; } .ipfs-filename-input { flex: 1; border: 1px solid #ddd; border-radius: 4px; padding: 5px 8px; font-family: 'Consolas', 'Monaco', monospace; font-size: 12px; background-color: white; transition: border-color 0.2s ease; min-width: 0; } .ipfs-filename-input:focus { outline: none; border-color: #4a90e2; } /* 防止窗口被遮挡的额外样式 */ @media screen and (max-height: 600px) { .ipfs-speed-test-window, .ipfs-gateway-manager { top: 10px; max-height: calc(100vh - 20px); } } `); // 创建悬停按钮 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 = `