IPFS CID Copy Helper

为IPFS链接添加CID复制功能,支持多种IPFS/IPNS格式和批量复制

当前为 2024-10-29 提交的版本,查看 最新版本

// ==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
// ==/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 = `
        <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
            <polyline points="15 18 9 12 15 6"></polyline>
        </svg>
    `;
    batchButtonsContainer.appendChild(toggleBtn);

    // 创建批量复制CID按钮
    const batchCopyBtn = document.createElement('div');
    batchCopyBtn.className = 'ipfs-batch-btn';
    batchCopyBtn.innerHTML = '批量复制 CID <span class="ipfs-copy-count">0</span>';
    batchButtonsContainer.appendChild(batchCopyBtn);

    // 创建批量复制文件名按钮
    const batchFilenameBtn = document.createElement('div');
    batchFilenameBtn.className = 'ipfs-batch-btn';
    batchFilenameBtn.innerHTML = '批量复制文件名 <span class="ipfs-copy-count">0</span>';
    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 = '批量复制下载链接 <span class="ipfs-copy-count">0</span>';
    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 = '批量复制文件名 <span class="ipfs-copy-count">' +
                    linkInfo.size + '</span>';
            }, 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);
})();