// ==UserScript== // @name IPFS CID Copy Helper // @namespace http://tampermonkey.net/ // @version 1.0 // @description 自动为网页中的 IPFS 链接添加 CID 复制功能,右下角可以显示批量操作窗口。 支持多种 IPFS/IPNS 格式和批量复制。 // @author cenglin123 // @match *://*/* // @grant GM_addStyle // @grant GM_registerMenuCommand // @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: flex; flex-direction: column; gap: 10px; z-index: 10000; transition: transform 0.3s ease; min-height: 100px; /* 设置固定的初始高度 */ } .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: #ff4444; 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); } `); // 创建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'; document.body.appendChild(batchButtonsContainer); // 创建收起/展开按钮 const toggleBtn = document.createElement('button'); toggleBtn.className = 'ipfs-toggle-btn'; toggleBtn.innerHTML = ` `; batchButtonsContainer.appendChild(toggleBtn); // 创建批量复制CID按钮 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); // 提取文件名的函数 function extractFilename(url, linkText) { // 从 URL 参数中获取文件名 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]; // 如果路径最后一部分不是 CID,则可能是文件名 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; } // 创建批量复制下载链接按钮 const batchDownloadBtn = document.createElement('div'); batchDownloadBtn.className = 'ipfs-batch-btn'; batchDownloadBtn.innerHTML = '批量复制下载链接 0'; batchButtonsContainer.appendChild(batchDownloadBtn); // 存储页面上找到的所有链接信息 const linkInfo = new Map(); // 存储CID和原始链接 // 存储收起状态 let isCollapsed = false; // 切换收起/展开状态 function toggleCollapse() { isCollapsed = !isCollapsed; batchButtonsContainer.classList.toggle('collapsed', isCollapsed); // 保存状态到localStorage localStorage.setItem('ipfsCopyHelperCollapsed', isCollapsed); } // 初始化收起状态 const savedCollapsedState = localStorage.getItem('ipfsCopyHelperCollapsed'); if (savedCollapsedState !== null) { isCollapsed = (savedCollapsedState === 'true'); batchButtonsContainer.classList.toggle('collapsed', isCollapsed); } // 配置选项 - 默认收起或展开 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'); } // 提取CID的函数 function extractCID(url) { // 匹配子域名形式的CID (bafy... or Qm...) const subdomainMatch = url.match(/^https?:\/\/(baf[a-zA-Z0-9]{1,}|Qm[a-zA-Z0-9]{44})\./i); if (subdomainMatch) { return subdomainMatch[1]; } // 匹配路径中的IPFS CID(包括 Qm 和 bafy 开头的格式) const ipfsPathMatch = url.match(/\/ipfs\/(Qm[a-zA-Z0-9]{44}|baf[a-zA-Z0-9]+)/i); if (ipfsPathMatch) { return ipfsPathMatch[1]; } // 匹配 IPNS 的密钥 const ipnsKeyMatch = url.match(/\/ipns\/(k51[a-zA-Z0-9]+)/i); if (ipnsKeyMatch) { return ipnsKeyMatch[1]; } return null; } // 检测链接类型 function detectLinkType(url) { if (url.includes('/ipns/') || url.includes('k51')) { return 'IPNS Key'; } return 'IPFS CID'; } // 扫描页面上的所有链接 function scanPageForLinks() { const links = document.getElementsByTagName('a'); const currentPageCID = extractCID(window.location.href); linkInfo.clear(); for (const link of links) { const cid = extractCID(link.href); if (cid && cid !== currentPageCID) { 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; }); if (count > 0) { batchCopyBtn.style.display = 'block'; batchDownloadBtn.style.display = 'block'; batchFilenameBtn.style.display = 'block'; } else { batchCopyBtn.style.display = 'none'; batchDownloadBtn.style.display = 'none'; batchFilenameBtn.style.display = 'none'; } } // 复制文本到剪贴板 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); }); } // 批量复制CID function batchCopyCIDs() { const cids = Array.from(linkInfo.keys()); console.log("批量复制的 CID:", cids); // 检查批量复制内容 if (cids.length > 0) { // 仅在有内容时复制 const formattedCIDs = cids.join('\n'); copyToClipboard(formattedCIDs, batchCopyBtn); } else { console.log("没有可复制的 CID。"); } } // 批量复制文件名 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; // 如果存在文件名且URL中没有filename参数,则添加 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); } } // 初始化页面扫描 let scanTimeout; function initPageScan() { if (scanTimeout) { clearTimeout(scanTimeout); } scanTimeout = setTimeout(scanPageForLinks, 1000); } // 监听页面变化 const observer = new MutationObserver((mutations) => { initPageScan(); }); observer.observe(document.body, { childList: true, subtree: true }); // 初始扫描 initPageScan(); // 监听所有链接的鼠标事件 document.addEventListener('mouseover', function(e) { const link = e.target.closest('a'); if (!link) return; const href = link.href; if (!href) return; const cid = extractCID(href); if (cid) { const linkType = detectLinkType(href); const rect = link.getBoundingClientRect(); copyBtn.style.display = 'block'; copyBtn.style.top = `${rect.bottom + window.scrollY + 5}px`; copyBtn.style.left = `${rect.left + (rect.width / 2) + window.scrollX}px`; copyBtn.textContent = `复制 ${linkType}`; copyBtn.onclick = () => copyToClipboard(cid, copyBtn); } }); // 鼠标移出事件处理 document.addEventListener('mouseout', function(e) { if (!e.target.closest('a') && !e.target.closest('.ipfs-copy-btn')) { copyBtn.style.display = 'none'; } }); // 防止按钮消失得太快 copyBtn.addEventListener('mouseover', function() { copyBtn.style.display = 'block'; }); copyBtn.addEventListener('mouseout', function(e) { if (!e.relatedTarget || !e.relatedTarget.closest('a')) { copyBtn.style.display = 'none'; } }); // 添加批量按钮的点击事件 batchCopyBtn.addEventListener('click', batchCopyCIDs); batchFilenameBtn.addEventListener('click', batchCopyFilenames); // 添加文件名复制按钮的点击事件 batchDownloadBtn.addEventListener('click', batchCopyDownloadLinks); toggleBtn.addEventListener('click', toggleCollapse); })();