// ==UserScript== // @name IPFS CID Copy Helper (v1.8) // @namespace http://tampermonkey.net/ // @version 1.8 // @description 自动为网页中的 IPFS 链接和文本添加 CID 复制功能,支持普通文本中的 CID。 // @author cenglin123 (modified) // @match *://*/* // @grant GM_addStyle // @grant GM_registerMenuCommand // @homepage https://github.com/cenglin123/ipfs-cid-copy-helper // @license MIT // @downloadURL none // ==/UserScript== (function() { 'use strict'; // 样式 GM_addStyle(` .ipfs-copy-btn { display: none; position: absolute; background: #4a90e2; color: white; padding: 5px 10px; border-radius: 4px; font-size: 12px; cursor: pointer; z-index: 10000; box-shadow: 0 2px 5px rgba(0,0,0,0.2); transform: translateX(-50%); } .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); } `); // 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 ]; // 创建UI元素 const copyBtn = document.createElement('div'); copyBtn.className = 'ipfs-copy-btn'; copyBtn.textContent = '复制 CID'; document.body.appendChild(copyBtn); const batchButtonsContainer = document.createElement('div'); batchButtonsContainer.className = 'ipfs-batch-buttons'; // 根据默认设置决定是否添加 collapsed 类 if (localStorage.getItem('ipfsCopyHelperDefaultCollapsed') === 'true') { 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); // 检查是否为 .crop.top 域名 function isCropTop(url) { try { const hostname = new URL(url).hostname; return hostname.endsWith('.crop.top'); } catch (e) { return false; } } // 提取CID函数 function extractCID(url) { try { const urlObj = new URL(url); // 定义要排除的 CID const excludedCIDs = [ 'bafybeiczsscdsbs7ffqz55asqdf3smv6klcw3gofszvwlyarci47bgf354', 'QmUNLLsPACCz1vLxQVkXqqLX5R1X345qqfHbsf67hvA3Nn' ]; let extractedCID = null; // 处理 crop.top 域名 if (isCropTop(url)) { 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; } } // 如果不是 crop.top 或没有从子域名中提取到 CID,继续其他匹配 if (!extractedCID) { // 匹配子域名形式 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 ipfsPathMatch = urlObj.pathname.match(/\/ipfs\/(baf[yk][a-zA-Z0-9]{55}|Qm[a-zA-Z0-9]{44})/i) || url.match(/\/ipfs\/(baf[yk][a-zA-Z0-9]{55}|Qm[a-zA-Z0-9]{44})/i); if (ipfsPathMatch) { extractedCID = ipfsPathMatch[1]; } if (!extractedCID) { const pathParts = urlObj.pathname.split('/'); for (let i = 0; i < pathParts.length; i++) { if (pathParts[i].toLowerCase() === 'ipfs' && i + 1 < pathParts.length) { const potentialCID = pathParts[i + 1]; if (potentialCID.match(/^(baf[yk][a-zA-Z0-9]{55}|Qm[a-zA-Z0-9]{44})$/i)) { extractedCID = potentialCID; break; } } } } // 匹配路径中的 IPNS key const ipnsKeyMatch = urlObj.pathname.match(/\/ipns\/(k51[a-zA-Z0-9]{59})/i) || url.match(/\/ipns\/(k51[a-zA-Z0-9]{59})/i); if (ipnsKeyMatch) { extractedCID = ipnsKeyMatch[1]; } // 匹配路径中的独立 IPNS key const ipnsPathMatch = urlObj.pathname.match(/(k51[a-zA-Z0-9]{59})/i); if (ipnsPathMatch) { extractedCID = ipnsPathMatch[1]; } } // 检查是否为排除的 CID if (extractedCID && excludedCIDs.includes(extractedCID.toLowerCase())) { return null; } return extractedCID; } catch (e) { console.error('URL解析错误:', e); return null; } } // 查找文本中的 CID function findCIDInText(text) { for (const pattern of CID_PATTERNS) { const match = text.match(pattern); if (match) { return { cid: match[1], type: match[1].startsWith('k51') ? 'IPNS Key' : 'IPFS CID' }; } } return null; } // 获取选中文本 function getSelectedText() { const selection = window.getSelection(); if (selection.rangeCount === 0) return null; const range = selection.getRangeAt(0); const text = range.toString().trim(); if (text.length === 0) return null; return { text: text, range: range }; } // 判断是否为文件浏览页面 function isIPFSBrowsingPage(url) { try { const pathname = new URL(url).pathname; return pathname.includes('/ipfs/') && pathname.split('/').length > 3; } catch (e) { return false; } } // 扫描页面函数 function scanPageForLinks() { const links = document.getElementsByTagName('a'); linkInfo.clear(); const currentPageCID = extractCID(window.location.href); const currentPageBase = window.location.origin + window.location.pathname.split('/').slice(0, -1).join('/'); for (const link of links) { const cid = extractCID(link.href); if (!cid || cid === currentPageCID) continue; try { const linkUrl = new URL(link.href); const linkBase = linkUrl.origin + linkUrl.pathname.split('/').slice(0, -1).join('/'); if (linkBase === currentPageBase) continue; } catch (e) { console.error('URL解析错误:', e); } const filename = extractFilename(link.href, link.textContent); linkInfo.set(cid, { type: detectLinkType(link.href), url: link.href, text: link.textContent.trim(), filename: filename }); } const count = linkInfo.size; const countElements = document.querySelectorAll('.ipfs-copy-count'); countElements.forEach(el => { el.textContent = count; }); // 修改:根据是否有 CID 来显示/隐藏整个浮窗 if (count > 0) { batchButtonsContainer.classList.add('visible'); [batchCopyBtn, batchDownloadBtn, batchFilenameBtn].forEach(btn => { btn.style.display = 'block'; }); } else { batchButtonsContainer.classList.remove('visible'); [batchCopyBtn, batchDownloadBtn, batchFilenameBtn].forEach(btn => { btn.style.display = 'none'; }); } } 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; } 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; navigator.clipboard.writeText(text).then(() => { button.textContent = '已复制!'; setTimeout(() => { button.textContent = originalText; }, 1000); }).catch(err => { console.error('复制失败:', err); button.textContent = '复制失败'; setTimeout(() => { button.textContent = originalText; }, 1000); }); } const linkInfo = new Map(); let isCollapsed = false; function toggleCollapse() { isCollapsed = !isCollapsed; batchButtonsContainer.classList.toggle('collapsed', isCollapsed); localStorage.setItem('ipfsCopyHelperCollapsed', isCollapsed); } function batchCopyCIDs() { const cids = Array.from(linkInfo.keys()); if (cids.length > 0) { const formattedCIDs = cids.join('\n'); copyToClipboard(formattedCIDs, batchCopyBtn); } } function batchCopyFilenames() { const filenames = Array.from(linkInfo.values()) .map(info => info.filename || '未知文件名') .filter(filename => filename !== '未知文件名'); if (filenames.length > 0) { const formattedFilenames = filenames.join('\n'); copyToClipboard(formattedFilenames, batchFilenameBtn); } else { batchFilenameBtn.textContent = '没有可用的文件名'; setTimeout(() => { batchFilenameBtn.innerHTML = '批量复制文件名 ' + linkInfo.size + ''; }, 1000); } } function batchCopyDownloadLinks() { const links = Array.from(linkInfo.values()).map(info => { let url = info.url; if (info.filename && !url.includes('?filename=')) { url += (url.includes('?') ? '&' : '?') + 'filename=' + encodeURIComponent(info.filename); } return url; }); if (links.length > 0) { const formattedLinks = links.join('\n'); copyToClipboard(formattedLinks, batchDownloadBtn); } } // 显示复制按钮 function showCopyButton(x, y, cid, type) { copyBtn.style.display = 'block'; copyBtn.style.top = `${y + window.scrollY + 5}px`; copyBtn.style.left = `${x + window.scrollX}px`; copyBtn.textContent = `复制 ${type}`; copyBtn.onclick = () => { navigator.clipboard.writeText(cid).then(() => { copyBtn.textContent = '已复制!'; setTimeout(() => { copyBtn.textContent = `复制 ${type}`; copyBtn.style.display = 'none'; }, 1000); }); }; } // 初始化文本选择功能 function initTextSelection() { document.addEventListener('mouseup', function(e) { if (e.target.classList.contains('ipfs-copy-btn')) return; setTimeout(() => { const selection = getSelectedText(); if (!selection) return; const cidInfo = findCIDInText(selection.text); if (cidInfo) { const rect = selection.range.getBoundingClientRect(); showCopyButton( rect.left + (rect.width / 2), rect.bottom, cidInfo.cid, cidInfo.type ); } }, 10); }); // 点击其他地方时隐藏按钮 document.addEventListener('mousedown', function(e) { if (!e.target.classList.contains('ipfs-copy-btn')) { copyBtn.style.display = 'none'; } }); } let scanTimeout; function initPageScan() { if (scanTimeout) { clearTimeout(scanTimeout); } scanTimeout = setTimeout(scanPageForLinks, 1000); } // 添加新的变量来跟踪状态 let currentHoveredLink = null; let isButtonHovered = false; let hideTimeout = null; // 用于检查鼠标是否在元素或其子元素上 function isMouseOverElement(element, event) { const rect = element.getBoundingClientRect(); return ( event.clientX >= rect.left && event.clientX <= rect.right && event.clientY >= rect.top && event.clientY <= rect.bottom ); } // 链接悬停处理 function hideButton() { if (hideTimeout) { clearTimeout(hideTimeout); } hideTimeout = setTimeout(() => { if (!isButtonHovered && !currentHoveredLink) { copyBtn.style.display = 'none'; } }, 100); // 添加100ms延迟 } // 链接悬停处理 document.addEventListener('mouseover', function(e) { const link = e.target.closest('a'); if (link) { currentHoveredLink = 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) { const linkType = detectLinkType(href); const rect = link.getBoundingClientRect(); showCopyButton( rect.left + (rect.width / 2), rect.bottom, linkCID, linkType ); } } }); document.addEventListener('mouseout', function(e) { const link = e.target.closest('a'); if (link) { if (!isMouseOverElement(link, e)) { currentHoveredLink = null; hideButton(); } } }); // 复制按钮自身的悬停处理 copyBtn.addEventListener('mouseover', function() { isButtonHovered = true; if (hideTimeout) { clearTimeout(hideTimeout); } }); copyBtn.addEventListener('mouseout', function(e) { isButtonHovered = false; // 检查鼠标是否移动到了链接上 const relatedTarget = e.relatedTarget; const isOverLink = relatedTarget && (relatedTarget.closest('a') === currentHoveredLink); if (!isOverLink) { hideButton(); } }); // 处理在按钮和链接之间移动的情况 document.addEventListener('mousemove', function(e) { const overLink = e.target.closest('a'); const overButton = e.target.closest('.ipfs-copy-btn'); if (!overLink && !overButton) { currentHoveredLink = null; isButtonHovered = false; hideButton(); } }); // 观察DOM变化 const observer = new MutationObserver(() => { initPageScan(); }); observer.observe(document.body, { childList: true, subtree: true }); // 添加菜单命令 GM_registerMenuCommand('切换右下角浮窗默认展开/收起状态', () => { const defaultCollapsed = localStorage.getItem('ipfsCopyHelperDefaultCollapsed'); const newDefault = defaultCollapsed === 'true' ? 'false' : 'true'; localStorage.setItem('ipfsCopyHelperDefaultCollapsed', newDefault); alert(`默认状态已更改为:${newDefault === 'true' ? '收起' : '展开'}`); }); // 检查默认配置和初始化 const defaultCollapsedState = localStorage.getItem('ipfsCopyHelperDefaultCollapsed'); if (defaultCollapsedState === 'false') { isCollapsed = false; batchButtonsContainer.classList.remove('collapsed'); } else if (defaultCollapsedState === 'true') { isCollapsed = true; batchButtonsContainer.classList.add('collapsed'); } // 绑定批量按钮事件 batchCopyBtn.addEventListener('click', batchCopyCIDs); batchFilenameBtn.addEventListener('click', batchCopyFilenames); batchDownloadBtn.addEventListener('click', batchCopyDownloadLinks); toggleBtn.addEventListener('click', toggleCollapse); // 启动文本选择功能和初始扫描 initTextSelection(); initPageScan(); })();