// ==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);
})();